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.

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- applescript-macos

Installs to ~/.agents/skills/applescript-macos/ for every project on this machine.

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

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.

~/.agents/skills/applescript-macos/SKILL.md

--- name: applescript-macos description: Automates macOS apps with AppleScript, JavaScript for Automation (JXA) and the Shortcuts CLI - scripting Finder, Mail, Calendar, Notes, Safari and… license: MIT ---

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

NeedUse
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 appshortcuts run "Name"
Files, text, network, JSONA shell or Python script; call osascript only for the app part
App with no dictionaryGUI scripting via System Events (fragile, last resort)
Run on a schedule or at loginlaunchd 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

  • osascript that 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 running when launching the app would be a surprise.
  • Return tab- or linefeed-separated text (or JSON from JXA) so callers can parse it.
  • Use whose clauses 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" and POSIX path of someAlias explicitly.

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

ErrorMeaningFix
-1743 “Not authorized to send Apple events”Automation permission deniedUser enables it in Privacy and Security, Automation
-1719 / “not allowed assistive access”GUI scripting without AccessibilityUser 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 outApp busy longer than the timeoutwith timeout of N seconds or smaller batches
-600 “Application isn’t running”App quit or crashed mid-scriptlaunch/activate first, or exit cleanly
-10004 privilege violationSandboxed or restricted commandRun 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.
  • osascript exit code is non-zero on errors; test the usage error path too.
  • For launchd jobs: launchctl print gui/$(id -u)/com.acme.archive-mail shows last exit status; check the log file after a kickstart.

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.yamlgenerated from this page

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.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem