Skip to content
macOSDeep Dive Published Updated 8 min readViews unavailable

Apple Events on macOS: Automation Consent, Delivery, and Safe Contracts

Design macOS Apple Event automation with explicit TCC consent, bounded event handlers, stable terminology, and observable cross-process failure handling.

Apple Events are a long-lived macOS interprocess automation protocol. They let one application ask another to perform a supported operation, such as opening a document or returning a value from a scriptable object model. That capability is useful for editors, asset pipelines, accessibility workflows, and user-authored automation. It is also a privacy boundary: an event sender can cause another app to reveal or change data through the recipient’s own privileges. Treat an Apple Event as a user-visible request crossing a process boundary, not as an ordinary in-process function call.

This distinction explains why modern macOS separates the ability to send an event from the target app’s scripting implementation. The sender needs a product reason and user authorization; the recipient must implement a constrained vocabulary and validate inputs. A successful tell statement proves only that some event reached a handler. It does not prove the handler’s semantics, the requested document identity, or the safety of subsequent side effects.

Choose the interaction contract first

Before writing AppleScript or calling an event API, state the user task in terms of the recipient’s documented behavior. Prefer an application-provided URL scheme, App Intent, command-line interface, or XPC contract when it provides a better supported operation. On macOS, App Intents can expose actions to user-built workflows in Shortcuts; other discovery surfaces vary by platform, and preconfigured App Shortcuts are not supported on Mac. Apple Events remain useful when a user explicitly needs to automate a scriptable desktop application. Avoid selecting the broadest protocol merely because it can reach another process.

Define request and response details before implementation: which target application is expected, what values are accepted, whether the operation can be repeated, what identifies the affected document, and how partial completion is reported. A script that says “save the front document” depends on mutable UI focus; a script that targets a stable document identifier and returns an explicit result is easier to test. Keep commands idempotent where possible, and make destructive actions require a confirmation in the product workflow rather than assuming automation consent means consent to every future action.

The sender also needs a timeout and cancellation policy. Apple Events can fail because the target is not running, is busy, has no matching handler, or is waiting for a modal user interaction. Do not block the main thread indefinitely while waiting for a target application. Present a progress state that can be canceled, and distinguish transport failure from a recipient’s application-level rejection.

If an app sends Apple Events to control another app, its bundle must include NSAppleEventsUsageDescription. The value should explain the actual task and the target app in plain language. “Automation required” tells the user little; “Send the selected image to Pixel Editor to open it for editing” connects the request to an action the user initiated. Avoid asking at first launch when the feature is not yet in use. Explain the action, then invoke the capability that causes macOS to present the authorization flow.

An illustrative Info.plist entry is:

<key>NSAppleEventsUsageDescription</key>
<string>Send the document you selected to your chosen editor so it can open it.</string>

The usage string is not an entitlement to silently automate every installed app. System policy and the user’s Automation choice determine whether the sender may control a particular recipient. A permission granted for one target should not be presented as authority over a different target. Do not search for or edit protected TCC database files to manufacture consent; that bypasses the security boundary and is not a supported administration method.

When the user denies access, preserve the rest of the app’s functionality. Explain which feature is unavailable, offer a user-controlled path to System Settings when appropriate, and avoid repeatedly triggering prompts. A reset with tccutil is a troubleshooting action for a test account, not a production recovery strategy or a way to make a user approve an operation. Authorization state can change while the app is running, so handle denial and cancellation on every operation instead of caching “allowed” as a permanent fact.

Keep AppleScript data separate from executable source

If an app invokes a script, do not concatenate user-provided strings into AppleScript source. Quoting rules, coercions, and script-language syntax create injection risk, and the generated text is difficult to audit. Prefer a fixed script with typed inputs or a narrow application API. Validate identifiers against an allowlist, constrain paths to the selected resources the user authorized, and avoid passing secrets through logs, command-line arguments, or error strings.

For a user-operated diagnostic from Terminal, a small script can help determine whether the current user can ask Finder to answer a harmless property query:

tell application "Finder"
    get name of startup disk
end tell

Run this only as an explicit test initiated by the user. An automation denial is a useful result: it means the app should not proceed with that feature. Do not advise disabling Gatekeeper, SIP, or TCC to “fix” normal consent behavior. Record the sender bundle identifier, the intended recipient, the requested operation category, and the authorization outcome, but never record full document contents, contact data, mailbox data, or script values that can contain private information.

Implement recipient handlers as a public API

A scriptable app should treat its Apple Event dictionary and scripting terminology as a public API surface. Names and object specifiers become part of user-authored scripts, so changing them can break workflows even when the GUI still works. Document supported commands, parameter types, reply shapes, and error behavior. Keep the model layer authoritative: a handler should resolve the requested object by stable identifier, verify that it belongs to the current account or document, and then call the same validated operation used by the app’s own interface.

Never trust that an event came from a friendly automation tool. Validate all required parameters, reject unsupported coercions, bound collection sizes, and verify that a requested path is in scope before reading or writing it. If the command mutates data, use the document’s normal transaction or undo mechanism and return a result that identifies what changed. In a multi-window app, do not silently fall back to whichever window happened to become key after the sender dispatched its request.

Make event handlers safe under reentrancy. A recipient may receive another event while the first command is awaiting disk or network work. Serialize operations that mutate shared state, but do not keep the UI unresponsive while they run. Define what happens if the user closes the document or signs out before completion. Return an actionable error code or message for expected failures instead of reporting success after a partial operation.

Diagnose the sender and the receiver separately

Begin with the sender’s own bundle identifier, signing identity, and NSAppleEventsUsageDescription in the built product, not only in the source project. Verify which target application the user selected and whether a current Automation decision exists in System Settings. Then inspect the recipient’s scripting dictionary or official automation documentation for the exact command vocabulary. A syntactically valid AppleScript can still call a nonexistent command or address an object the recipient does not expose.

For a controlled local check, use osascript with a read-only request to a known target and observe the exact error. A TCC denial, missing application, target timeout, and “event not handled” are different conditions; report them distinctly. Test with a clean user profile where permissions begin unset, then test allow, deny, revoke, target quit, target busy, and target upgrade. Confirm the app remains usable after every denied or unavailable path.

Do not infer success from the sender process exiting with status zero if the script itself swallowed an error. Check the returned descriptor and the recipient’s resulting state. For a state-changing command, verify a stable postcondition, such as the expected document being open or the requested export appearing at the user-selected destination. Use temporary test data and clean it up only after recording results.

Production checklist

Ship a small, documented Apple Event contract rather than a generic remote-control surface. Ask for permission only when a user invokes the feature; retain a useful denial path; make requests bounded and cancelable; and test against the exact recipient versions your product supports. Include privacy review for every field returned by a handler. Treat Automation grants as revocable, recipient-specific consent and avoid retaining data returned by automation longer than the task requires.

Finally, audit where Apple Events stop being the right choice. A simple request that another app open a URL may be better expressed through NSWorkspace; a system action intended for Shortcuts may belong in App Intents; a high-volume internal protocol between components should use a narrow XPC service. The reliable implementation is the one whose permissions, user experience, and failure model match the real task, not the one with the most powerful event vocabulary.

Related:

Sources:

Comments