AppleScript and macOS Automation skill for Codex
Script Mac apps through dictionaries, not clicks. Codex automates Mac apps through their scripting dictionaries with AppleScript or JXA, passes inputs as osascript arguments and returns parseable text. It explains the Automation and Accessibility prompts only you can approve, keeps GUI scripting as a last resort, drafts mail instead of sending it, and installs launchd agents only after you have seen the plist.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- applescript-macos Installs to ~/.agents/skills/applescript-macos/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install applescript-macos The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project applescript-macos Installs to .agents/skills/applescript-macos/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/applescript-macos.zipunzip -o applescript-macos.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) applescript-macos PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $applescript-macos, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Automates macOS apps with AppleScript, JavaScript for Automation (JXA) and the Shortcuts CLI - scripting Finder, Mail, Calendar, Notes, Safari and Music through their dictionaries, running scripts with osascript and arguments, handling Automation and Accessibility permission prompts, GUI scripting via System Events only as a last resort, and scheduling with launchd. Use when asked to automate a Mac app, write an AppleScript or JXA script, run a Shortcut from the terminal, script repetitive Finder or Mail chores, or fix an osascript error such as -1743 not authorized.
Use it when
- A Mac app chore (Mail, Finder, Calendar, Notes, Music) should be scripted
- A Shortcut must run from the terminal or another script
- An osascript call fails with a permission or object error
- A Mac automation should run daily or at login
Use something else when
- The automation targets Windows desktops: use windows-automation
- You only need to react to files landing in a folder: use desktop-file-watchers
How to check the result
Run read-only scripts from the terminal and parse their output, test write scripts against a test calendar, note folder or draft to yourself, and check launchctl print for the agent's last exit status.
The mistake it prevents
Reaching for System Events GUI clicks when the app has a scripting dictionary, which breaks on the next app update or a different system language.
A Mac automation is done when it talks to apps through their scripting dictionaries (not by clicking), takes input as arguments, returns plain text the caller can parse, survives the app not running, documents every permission it needs, and asks before it sends, deletes or changes anything the user cannot easily undo.
Pick the mechanism
| Need | Use |
|---|---|
| Control a scriptable app (Finder, Mail, Calendar, Notes, Music, Safari, Keynote) | AppleScript, or JXA if the team prefers JavaScript |
| Run an existing Shortcut or one the user builds in the Shortcuts app | shortcuts run "Name" |
| Files, text, network, JSON | A shell or Python script; call osascript only for the app part |
| App with no dictionary | GUI scripting via System Events (fragile, last resort) |
| Run on a schedule or at login | launchd agent |
| React to files appearing in a folder | $desktop-file-watchers |
Check scriptability first: sdef /Applications/Notes.app | head -50. No output or an
error means the app has no dictionary. Open Script Editor, File, Open Dictionary for a
readable view.
Codex realities
osascriptthat targets other apps sends Apple Events. The first call triggers a macOS consent prompt (“Terminal wants to control Mail”) that only the user can approve, in System Settings, Privacy and Security, Automation. The Codex sandbox may also block Apple Events; if a call fails with error -1743 or -600 or simply hangs, stop and ask the user to run it or approve running outside the sandbox. Never try to change TCC settings or databases yourself.- GUI scripting additionally needs Accessibility permission for the host app; ask the user to grant it.
- Read-only queries (list calendars, read selected Finder items) are safe to test. Anything that sends mail, deletes files, empties Trash, or modifies events needs the user’s explicit confirmation of the exact action first.
Running scripts
osascript -e 'tell application "Finder" to get name of every disk'
osascript ~/scripts/archive-mail.applescript "Receipts" 30 # arguments
osascript -l JavaScript ~/scripts/today.js # JXA
osacompile -o archive-mail.scpt archive-mail.applescript # compile (faster start)
shortcuts list
shortcuts run "Resize for Web" -i ~/Desktop/photo.heic -o ~/Desktop/out.jpg
Scripts receive arguments through on run argv (AppleScript) or function run(argv) (JXA).
Whatever the run handler returns is printed on stdout; errors go to stderr with a non-zero
exit code, so shells and Python can treat them like any other command.
AppleScript template
on run argv
if (count of argv) < 1 then error "usage: osascript count-unread.applescript MAILBOX" number 64
set boxName to item 1 of argv
if application "Mail" is not running then return "Mail not running"
tell application "Mail"
set results to {}
repeat with acct in accounts
try
set mb to mailbox boxName of acct
set end of results to (name of acct) & tab & (unread count of mb)
end try
end repeat
end tell
set AppleScript's text item delimiters to linefeed
return results as text
end run
Patterns worth keeping:
- Check
application "X" is runningwhen launching the app would be a surprise. - Return tab- or linefeed-separated text (or JSON from JXA) so callers can parse it.
- Use
whoseclauses to filter inside the app (every message of inbox whose read status is false) instead of looping over thousands of objects; each Apple Event round trip is slow. - Wrap long operations in
with timeout of 300 seconds ... end timeout; the default is 120 s. - Pass paths to the shell with
quoted form of:do shell script "ls -l " & quoted form of POSIX path of f. - Convert between
POSIX file "/Users/me/x.pdf"andPOSIX path of someAliasexplicitly.
JXA template
function run(argv) {
const cal = Application('Calendar');
const start = new Date(); start.setHours(0, 0, 0, 0);
const end = new Date(start); end.setDate(end.getDate() + 1);
const out = [];
for (const c of cal.calendars()) {
const evs = c.events.whose({ _and: [{ startDate: { _greaterThan: start } },
{ startDate: { _lessThan: end } }] })();
for (const e of evs) out.push({ calendar: c.name(), title: e.summary(), start: e.startDate().toISOString() });
}
return JSON.stringify(out);
}
JXA properties are method calls (e.summary()), whose takes objects with _greaterThan,
_contains and similar, and Application.currentApplication() with
includeStandardAdditions = true gives doShellScript, displayNotification and
pathTo. Documentation is thin; when JXA fights you, write that piece in AppleScript.
Common recipes
-- Notification (no permission prompt beyond Notifications)
display notification "Backup finished" with title "Nightly job"
-- Selected Finder items as POSIX paths
tell application "Finder" to set sel to selection as alias list
repeat with f in sel
log POSIX path of f
end repeat
-- Create a note (writes data: confirm first)
tell application "Notes" to make new note at folder "Notes" with properties {name:"Standup", body:"<h1>Standup</h1><p>...</p>"}
-- Draft, not send: let the user review in Mail
tell application "Mail"
set m to make new outgoing message with properties {subject:"Weekly report", content:"See attached.", visible:true}
tell m to make new to recipient with properties {address:"[email protected]"}
end tell
Default to creating drafts (visible:true, no send) for mail. Add send m only after
the user has confirmed recipients and content for that run or explicitly approved
unattended sending.
GUI scripting (last resort)
tell application "System Events" to tell process "SomeApp"
set frontmost to true
click menu item "Export…" of menu "File" of menu bar 1
delay 0.5
keystroke "report" & return
end tell
It breaks with app updates, localisation, window position and timing. Prefer menu items
over coordinates, wait on UI elements existing (repeat until exists window "Export")
instead of fixed delays, and tell the user it is brittle.
Scheduling with launchd
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>Label</key><string>com.acme.archive-mail</string>
<key>ProgramArguments</key><array>
<string>/usr/bin/osascript</string>
<string>/Users/me/scripts/archive-mail.scpt</string><string>Receipts</string>
</array>
<key>StartCalendarInterval</key><dict><key>Hour</key><integer>7</integer><key>Minute</key><integer>30</integer></dict>
<key>StandardOutPath</key><string>/Users/me/Library/Logs/archive-mail.log</string>
<key>StandardErrorPath</key><string>/Users/me/Library/Logs/archive-mail.log</string>
</dict></plist>
plutil -lint ~/Library/LaunchAgents/com.acme.archive-mail.plist
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.acme.archive-mail.plist
launchctl kickstart -k gui/$(id -u)/com.acme.archive-mail # run now to test
launchctl bootout gui/$(id -u)/com.acme.archive-mail # remove
Installing an agent changes the user’s login environment: show the plist and get a yes before bootstrapping. Use a LaunchAgent (user session), not a LaunchDaemon, for anything that talks to GUI apps. launchd runs jobs missed during sleep at wake.
Errors you will see
| Error | Meaning | Fix |
|---|---|---|
| -1743 “Not authorized to send Apple events” | Automation permission denied | User enables it in Privacy and Security, Automation |
| -1719 / “not allowed assistive access” | GUI scripting without Accessibility | User grants Accessibility to the host app |
| -1728 “Can’t get …” | Object does not exist (wrong name, empty selection) | Check existence with exists, guard with try |
| -1712 AppleEvent timed out | App busy longer than the timeout | with timeout of N seconds or smaller batches |
| -600 “Application isn’t running” | App quit or crashed mid-script | launch/activate first, or exit cleanly |
| -10004 privilege violation | Sandboxed or restricted command | Run from a different host app or via Shortcuts |
Verification
- Run each read-only script from the terminal and check stdout parses as intended.
- For writes, run against a test object (a test calendar, a “Codex test” note folder, draft mail to the user’s own address) and have the user confirm the result.
osascriptexit code is non-zero on errors; test the usage error path too.- For launchd jobs:
launchctl print gui/$(id -u)/com.acme.archive-mailshows last exit status; check the log file after akickstart.
Handoff
**Script:** ~/scripts/archive-mail.applescript (compiled to .scpt)
**Does:** moves Mail messages older than 30 days from "Receipts" to "Receipts/Archive"
**Run:** osascript ~/scripts/archive-mail.scpt Receipts 30
**Permissions:** Automation, Terminal controls Mail (granted by user on first run)
**Schedule:** launchd com.acme.archive-mail, daily 07:30 (installed after approval)
**Does not:** send mail or delete messages What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "AppleScript and macOS Automation"
short_description: "Script Mac apps through dictionaries, not clicks"
default_prompt: "$applescript-macos Write an AppleScript that saves every PDF attachment in my Receipts mailbox to a folder" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.