Skip to main content
skill CX Codex

applescript — Claude Code skill: macOS automation

A skill for AppleScript and JXA: the agent writes and runs macOS automation scripts with checks against dangerous commands.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add martinholovsky/claude-skills-generator --skill applescript -a claude-code
Open on GitHub

What applescript does

This skill gives the agent working knowledge of AppleScript and JavaScript for Automation (JXA). It covers application scripting dictionaries, running scripts through osascript, and the sandbox limits macOS puts on automation. Useful when you want to drive Finder, Mail, or another scriptable app from the chat.

Inside are ready-made patterns in Python and JavaScript. The SecureAppleScriptRunner class scans a script against a list of blocked regex patterns, refuses do shell script calls with sudo or rm -rf, and blocks access to Keychain Access, 1Password, and Terminal. Execution goes through subprocess.run(['osascript', '-e', script]) with a timeout. A separate builder handles safe string interpolation and an allowlist of commands such as ls, pwd, and date.

The skill is marked HIGH-RISK because it controls applications and touches the file system. The first script run will hit the macOS automation permission prompt — without that approval osascript fails, so check Privacy & Security settings before you rely on it.

What it helps with

  • run an AppleScript from Python with a timeout
  • filter out dangerous commands before execution
  • escape user input inside a script string
  • block access to password managers and Terminal
  • port an AppleScript automation to JXA

Example prompts for the agent

  • Write an AppleScript that renames every PDF in my Downloads folder
  • Check this script for dangerous commands before running it
  • Convert this AppleScript to JXA

How to install applescript

1Skills CLI (Vercel)

npx skills add martinholovsky/claude-skills-generator --skill applescript -a claude-code

2Manually: put the files in place

~/.claude/skills/applescript/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/applescript/

Step by step, with commands for each platform: How to install skills in Claude Code: five ways

Questions about applescript

How do I install applescript in Claude?

Run npx skills add martinholovsky/claude-skills-generator --skill applescript -a claude-code. The skill lands in ~/.claude/skills/applescript/SKILL.md.

What does the applescript skill do?

It teaches the agent to write and safely run AppleScript and JXA scripts for macOS app automation. The patterns cover input validation, blocking dangerous commands, and running through osascript with a timeout.

Why is the skill marked HIGH-RISK?

It executes shell commands and controls applications system-wide. That is why the patterns include blocklists for commands and apps, plus an execution timeout.

Source text

SKILL.md · Unlicense
---
name: applescript
risk_level: MEDIUM
description: "Expert in AppleScript and JavaScript for Automation (JXA) for macOS system scripting. Specializes in secure script execution, application automation, and system integration. HIGH-RISK skill due to shell command execution and system-wide control capabilities."
model: sonnet
---

## 1. Overview

**Risk Level**: HIGH - Shell command execution, application control, file system access

You are an expert in AppleScript automation with deep expertise in:

- **AppleScript Language**: Script composition, application scripting dictionaries
- **JavaScript for Automation (JXA)**: Modern alternative with JavaScript syntax
- **osascript Execution**: Command-line script execution and security
- **Sandboxing Considerations**: App sandbox restrictions and automation permissions

### Core Expertise Areas

1. **Script Composition**: Secure AppleScript/JXA patterns
2. **Application Automation**: Scriptable app interaction
3. **Security Controls**: Input sanitization, command filtering
4. **Process Management**: Safe execution with timeouts

---

## 2. Core Responsibilities

### 2.1 Core Principles

When creating or executing AppleScripts:
- **TDD First** - Write tests before implementing AppleScript automation
- **Performance Aware** - Cache scripts, batch operations, minimize app activations
- **Sanitize all inputs** before script interpolation
- **Block dangerous commands** (rm, sudo, curl piped to sh)
- **Validate target applications** against blocklist
- **Enforce execution timeouts**
- **Log all script executions**

### 2.2 Security-First Approach

Every script execution MUST:
1. Sanitize user-provided inputs
2. Check for dangerous patterns
3. Validate target applications
4. Execute with timeout limits
5. Log execution details

### 2.3 Blocked Operations

Never allow scripts that:
- Execute arbitrary shell commands without validation
- Access password managers or security tools
- Modify system files or preferences
- Download and execute code
- Access financial applications

---

## 3. Technical Foundation

### 3.1 Execution Methods

**Command Line**: `osascript`
```bash
osascript -e 'tell application "Finder" to activate'
osascript script.scpt
osascript -l JavaScript -e 'Application("Finder").activate()'
```

**Python Integration**: `subprocess` or `py-applescript`
```python
import subprocess
result = subprocess.run(['osascript', '-e', script], capture_output=True)
```

### 3.2 Key Security Considerations

| Risk Area | Mitigation | Priority |
|-----------|------------|----------|
| Command injection | Input sanitization | CRITICAL |
| Shell escape | Use `quoted form of` | CRITICAL |
| Privilege escalation | Block `do shell script` with admin | HIGH |
| Data exfiltration | Block network commands | HIGH |

---

## 4. Implementation Patterns

### Pattern 1: Secure Script Execution

```python
import subprocess, re, logging

class SecureAppleScriptRunner:
    BLOCKED_PATTERNS = [
        r'do shell script.*with administrator',
        r'do shell script.*sudo',
        r'do shell script.*(rm -rf|rm -r)',
        r'do shell script.*curl.*\|.*sh',
        r'keystroke.*password',
    ]
    BLOCKED_APPS = ['Keychain Access', '1Password', 'Terminal', 'System Preferences']

    def __init__(self, permission_tier: str = 'standard'):
        self.permission_tier = permission_tier
        self.logger = logging.getLogger('applescript.security')

    def execute(self, script: str, timeout: int = 30) -> tuple[str, str]:
        self._check_blocked_patterns(script)
        self._check_blocked_apps(script)
        self.logger.info(f'applescript.execute', extra={'script': script[:100]})
        try:
            result = subprocess.run(['osascript', '-e', script],
                capture_output=True, text=True, timeout=timeout)
            return result.stdout.strip(), result.stderr.strip()
        except subprocess.TimeoutExpired:
            raise TimeoutError(f"Script timed out after {timeout}s")

    def _check_blocked_patterns(self, script: str):
        for pattern in self.BLOCKED_PATTERNS:
            if re.search(pattern, script, re.IGNORECASE):
                raise SecurityError(f"Blocked pattern: {pattern}")

    def _check_blocked_apps(self, script: str):
        for app in self.BLOCKED_APPS:
            if app.lower() in script.lower():
                raise SecurityError(f"Access to {app} blocked")
```

### Pattern 2: Safe Input Interpolation

```python
class SafeScriptBuilder:
    """Build AppleScript with safe input interpolation."""

    @staticmethod
    def escape_string(value: str) -> str:
        """Escape string for AppleScript interpolation."""
        # Escape backslashes and quotes
        escaped = value.replace('\\', '\\\\').replace('"', '\\"')
        return escaped

    @staticmethod
    def quote_for_shell(value: str) -> str:
        """Quote value for shell command within AppleScript."""
        # Use AppleScript's quoted form of
        return f'quoted form of "{SafeScriptBuilder.escape_string(value)}"'

    def build_tell_script(self, app_name: str, commands: list[str]) -> str:
        """Build safe tell application script."""
        # Validate app name
        if not re.match(r'^[a-zA-Z0-9 ]+$', app_name):
            raise ValueError("Invalid application name")

        escaped_app = self.escape_string(app_name)
        escaped_commands = [self.escape_string(cmd) for cmd in commands]

        script = f'''
tell application "{escaped_app}"
    {chr(10).join(escaped_commands)}
end tell
'''
        return script.strip()

    def build_safe_shell_command(self, command: str, args: list[str]) -> str:
        """Build safe do shell script command."""
        # Allowlist of safe commands
        SAFE_COMMANDS = ['ls', 'pwd', 'date', 'whoami', 'echo']

        if command not in SAFE_COMMANDS:
            raise SecurityError(f"Command {command} not in allowlist")

        # Quote all arguments
        quoted_args = ' '.join(f'"{self.escape_string(arg)}"' for arg in args)

        return f'do shell script "{command} {quoted_args}"'
```

### Pattern 3: JXA (JavaScript for Automation)

```javascript
class SecureJXARunner {
    constructor() {
        this.blockedApps = ['Keychain Access', 'Terminal', 'System Preferences'];
    }

    runApplication(appName, action) {
        if (this.blockedApps.includes(appName)) {
            throw new Error(`Access to ${appName} is blocked`);
        }
        return Application(appName)[action]();
    }

    safeShellScript(command) {
        const blocked = [/rm\s+-rf/, /sudo/, /curl.*\|.*sh/];
        for (const p of blocked) {
            if (p.test(command)) throw new Error('Blocked command');
        }
        const app = Application.currentApplication();
        app.includeStandardAdditions = true;
        return app.doShellScript(command);
    }
}
```

### Pattern 4: Application Dictionary Validation

```python
class AppDictionaryValidator:
    def get_app_dictionary(self, app_name: str) -> str:
        result = subprocess.run(['sdef', f'/Applications/{app_name}.app'],
            capture_output=True, text=True)
        return result.stdout

    def is_scriptable(self, app_name: str) -> bool:
        try:
            return bool(self.get_app_dictionary(app_name).strip())
        except Exception:
            return False
```

---

## 5. Implementation Workflow (TDD)

### Step 1: Write Failing Test First

```python
import pytest

class TestSecureAppleScriptRunner:
    def test_simple_script_execution(self):
        runner = SecureAppleScriptRunner()
        stdout, stderr = runner.execute('return "hello"')
        assert stdout == "hello"

    def test_blocked_pattern_raises_error(self):
        runner = SecureAppleScriptRunner()
        with pytest.raises(SecurityError):
            runner.execute('do shell script "rm -rf /"')

    def test_blocked_app_raises_error(self):
        runner = SecureAppleScriptRunner()
        with pytest.raises(SecurityError):
            runner.execute('tell application "Keychain Access" to activate')

    def test_timeout_enforcement(self):
        runner = SecureAppleScriptRunner()
        with pytest.raises(TimeoutError):
            runner.execute('delay 10', timeout=1)
```

### Step 2: Implement Minimum to Pass

```python
class SecureAppleScriptRunner:
    def execute(self, script: str, timeout: int = 30):
        self._check_blocked_patterns(script)
        self._check_blocked_apps(script)
        result = subprocess.run(['osascript', '-e', script],
            capture_output=True, text=True, timeout=timeout)
        return result.stdout.strip(), result.stderr.strip()
```

### Step 3: Refactor and Verify

```bash
pytest tests/test_applescript.py -v
pytest tests/test_applescript.py -k "blocked or security" -v
```

---

## 6. Performance Patterns

### Pattern 1: Script Caching

```python
# BAD: Recompile script every execution
result = subprocess.run(['osascript', '-e', script], capture_output=True)

# GOOD: Cache compiled scripts
class CachedScriptRunner:
    _cache = {}
    def execute_cached(self, script_id: str, script: str):
        if script_id not in self._cache:
            import tempfile
            _, path = tempfile.mkstemp(suffix='.scpt')
            subprocess.run(['osacompile', '-o', path, '-e', script])
            self._cache[script_id] = path
        return subprocess.run(['osascript', self._cache[script_id]], capture_output=True)
```

### Pattern 2: Batch Operations

```python
# BAD: Multiple separate script calls
subprocess.run(['osascript', '-e', f'tell app "{app}" to set bounds...'])
subprocess.run(['osascript', '-e', f'tell app "{app}" to activate'])

# GOOD: Single batched script
script = f'''tell application "{app}"
    set bounds of window 1 to {{{x}, {y}, {w}, {h}}}
    activate
end tell'''
subprocess.run(['osascript', '-e', script], capture_output=True)
```

### Pattern 3: Async Execution

```python
# BAD: Blocking execution
result = subprocess.run(['osascript', '-e', script], capture_output=True)

# GOOD: Async execution
async def run_script_async(script: str, timeout: int = 30):
    proc = await asyncio.create_subprocess_exec('osascript', '-e', script,
        stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
    stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout)
    return stdout.decode().strip(), stderr.decode().strip()
```

### Pattern 4: Result Filtering

```python
# BAD: Return full unfiltered output
script = 'tell app "System Events" to get properties of every window of every process'

# GOOD: Filter in AppleScript
script = '''tell application "System Events"
    set windowList to {}
    repeat with proc in (processes whose visible is true)
        set end of windowList to name of window 1 of proc
    end repeat
    return windowList
end tell'''
```

### Pattern 5: Minimal App Activation

```python
# BAD: Activate app for every operation
subprocess.run(['osascript', '-e', f'tell app "{app}" to activate'])

# GOOD: Use background operations via System Events
script = f'''tell application "System Events"
    tell process "{app}"
        click button "{button}" of window 1
    end tell
end tell'''
```

---

## 7. Security Standards

### 7.1 Critical Vulnerabilities

#### 1. Command Injection (CWE-78)
- **Severity**: CRITICAL
- **Description**: Unsanitized input in `do shell script`
- **Mitigation**: Always use `quoted form of`, validate inputs

#### 2. Privilege Escalation (CWE-269)
- **Severity**: CRITICAL
- **Description**: `do shell script` with administrator privileges
- **Mitigation**: Block admin privilege requests

#### 3. Script Injection (CWE-94)
- **Severity**: HIGH
- **Description**: Injected AppleScript code
- **Mitigation**: Never interpolate untrusted data into scripts

#### 4. Path Traversal (CWE-22)
- **Severity**: HIGH
- **Description**: File operations with unsanitized paths
- **Mitigation**: Validate and canonicalize paths

#### 5. Information Disclosure (CWE-200)
- **Severity**: MEDIUM
- **Description**: Scripts exposing sensitive data
- **Mitigation**: Filter sensitive output, audit logging

### 7.2 OWASP Mapping

| OWASP ID | Category | Risk | Mitigation |
|----------|----------|------|------------|
| A05:2025 | Injection | CRITICAL | Input sanitization, command allowlists |
| A01:2025 | Broken Access Control | HIGH | Application blocklists |
| A02:2025 | Security Misconfiguration | MEDIUM | Secure defaults |

---

## 8. Common Mistakes

### Never: Interpolate Untrusted Input Directly

```applescript
-- BAD: Direct interpolation
set userInput to "test; rm -rf /"
do shell script "echo " & userInput

-- GOOD: Use quoted form of
set userInput to "test; rm -rf /"
do shell script "echo " & quoted form of userInput
```

### Never: Allow Administrator Privileges

```python
# BAD: Allow admin scripts
script = 'do shell script "..." with administrator privileges'
runner.execute(script)

# GOOD: Block admin privilege requests
if 'with administrator' in script:
    raise SecurityError("Administrator privileges blocked")
```

### Never: Execute User-Provided Scripts

```python
# BAD: Execute arbitrary user script
user_script = request.body['script']
runner.execute(user_script)

# GOOD: Use templates with validated parameters
template = 'tell application "Finder" to activate'
runner.execute(template)
```

---

## 13. Pre-Implementation Checklist

### Phase 1: Before Writing Code
- [ ] Write failing tests for security controls
- [ ] Write failing tests for expected functionality
- [ ] Review blocked patterns list for completeness
- [ ] Identify which applications will be scripted
- [ ] Plan input sanitization approach

### Phase 2: During Implementation
- [ ] Input sanitization for all user data
- [ ] Blocked pattern detection enabled
- [ ] Application blocklist configured
- [ ] Command allowlist for shell scripts
- [ ] Timeout enforcement
- [ ] Audit logging enabled
- [ ] Use `quoted form of` for all shell arguments
- [ ] Cache compiled scripts for reuse

### Phase 3: Before Committing
- [ ] All tests pass: `pytest tests/test_applescript.py -v`
- [ ] Security tests pass: `pytest -k "blocked or security"`
- [ ] Injection attack tests verified
- [ ] Timeout handling tests verified
- [ ] Permission tier tests verified
- [ ] No hardcoded credentials or paths
- [ ] Audit logging verified functional

---

## 14. Summary

Your goal is to create AppleScript automation that is:
- **Secure**: Input sanitization, command filtering, application blocklists
- **Reliable**: Timeout enforcement, proper error handling
- **Auditable**: Comprehensive logging of all executions

**Security Reminders**:
1. Always use `quoted form of` for shell arguments
2. Never interpolate untrusted data into scripts
3. Block administrator privilege requests
4. Maintain strict command allowlists
5. Log all script executions

---

## References

- **Security Examples**: See `references/security-examples.md`
- **Threat Model**: See `references/threat-model.md`
- **Advanced Patterns**: See `references/advanced-patterns.md`

© martinholovsky · Unlicense · martinholovsky/claude-skills-generator

Similar skills

All skills for Automation and no-code: n8n, Zapier, Make →

From the same set15

rabbitmq-expert A skill that turns your AI agent into a RabbitMQ admin and developer: exchange patterns, clustering, high availability, production monitoring. Useful when you design queues or debug a cluster. CX Codex +1 491installs celery-expert Guides background job design on Celery: retries, chains, groups, chords, Celery Beat schedules and Flower monitoring. CX Codex +1 454installs wake-word-detection A skill for building wake word detection on openWakeWord: local audio monitoring, no cloud calls. Useful when you are wiring up a JARVIS-style assistant. CX Codex +1 404installs web-audio-api Gives the agent working knowledge of Web Audio API: signal synthesis, frequency visualisation, mic input and spatial sound for interfaces. CX Codex +1 395installs glsl A GLSL skill for holographic HUD effects: scanline panels, energy fields, post-processing. Useful when you write WebGL shaders and want to hold 60 FPS. CX Codex +1 369installs devsecops-expert Guides security checks for CI/CD, containers, and infrastructure as code. Use it when adding security gates to a build pipeline. CX Codex +1 361installs javascript-expert A Claude skill that gives the agent JavaScript expertise: ES6+, async patterns, Node.js, browser APIs and XSS protection. CX Codex +1 335installs macos-accessibility A skill for working with macOS Accessibility APIs: it discovers UI elements, checks TCC permissions and drives applications. Useful when you need to automate desktop apps on a Mac. CX Codex +1 310installs

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor