{
  "@context": "https://schema.org",
  "@id": "https://aggr.aymericbeaumet.com/items/steipete-me/2025-07-03-making-applescript-work-in-macos-cli-tools-the-undocumented/#webpage",
  "@type": [
    "WebPage",
    "ArchiveComponent"
  ],
  "articleBody": "![](https://steipete.me/assets/img/2025/applescript-cli-macos-complete-guide/header.png)\n\n*Or: How I Learned to Stop Fighting TCC and Embrace the Info.plist*\n\n**TL;DR:** If you’re building a macOS CLI that uses AppleScript, you need to embed an Info.plist into your binary, sign it with proper entitlements, and optionally use the undocumented `responsibility_spawnattrs_setdisclaim` API to avoid permission dialogs that blames the hosting app.\n\nThis all started with Cursor being annoying. You know how it goes - you’re in the zone, AI is looping through its tasks, and then bam! The inline terminal opens something blocking (file watcher, dev server, …) and the whole loop stops. I have to manually click around to get things moving again.\n\nMy solution? Build an MCP that controls an external terminal. That way, even when commands block, Cursor’s loop keeps running. I called it [Terminator](https://github.com/steipete/Terminator) - ‘cause who doesn’t love a good terminal/Terminator pun?\n\n## The Evolution of a Hack\n\nMy first attempt was [pure AppleScript](https://github.com/steipete/Terminator/blob/main/scripts/terminator.scpt) - simple, direct terminal automation. It worked! Well, sort of… The script needed window focus to function, which meant it would constantly steal focus while Cursor was running.\n\nThe focus-stealing got so bad that at one point, the AI started writing its own AppleScript to detect which app was in the foreground. When it discovered Chrome was blocking its terminal access, it simply… killed all my Chrome windows. That was [the first time I apologized to an AI agent](https://steipete.me/posts/2025/when-ai-meets-madness-peters-16-hour-days#the-gemini-chrome-massacre). Clearly, I needed a better solution.\n\nThat’s when I fell down the rabbit hole. Getting AppleScript to work in a CLI tool turned out to be a maze of undocumented APIs, security permissions, and macOS quirks that nobody warns you about.\n\nSure, I could have used Apple’s [`osascript`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/osascript) command and called it a day. But where’s the fun in that? Plus, AppKit’s AppleScript API gives you much better error handling - if you can get it working.\n\n## Plot Twist: Enter Claude Code\n\nHere’s the thing - I never actually finished Terminator. Why? Because [Claude Code](https://docs.anthropic.com/en/docs/claude-code) came along and made it obsolete. As a massive Claude Code fanboy (seriously, [check](https://steipete.me/posts/2025/claude-code-is-my-computer/) [out](https://steipete.me/posts/2025/commanding-your-claude-code-army/) [my](https://steipete.me/posts/command-your-claude-code-army-reloaded/) [posts](https://steipete.me/posts/2025/vibe-meter-2-claude-code-usage-calculation/)), I ditched Cursor. Claude Code doesn’t have these inline terminal blocking issues.\n\nI’m writing this here since future you or me will stumble into the same issue eventually, and hopefully you can just drag this URL into Claude Code and it’ll fix everything up. Let Terminator lurk unfinished in my GitHub.\n\n## The Problem: Terminal Gets All the Blame\n\nHere’s what happens when you naively use `NSAppleScript` in a CLI tool:\n\n```swift\nlet script = NSAppleScript(source: \"tell application \\\"Finder\\\" to activate\")\nscript?.executeAndReturnError(nil)  // Spoiler: This won't work as expected\n```\n\nYou’ll either get:\n\n1. Silent failure (no error, no result, no nothing)\n2. A permission dialog that says “Terminal wants to control Finder/Cursor” (not your cli)\n3. Error -1750 (errOSASystemError) with zero helpful context\n\nThe root cause? macOS’s security model requires proper app identification through bundle IDs, code signing, and entitlements. Without these, your CLI tool is just an anonymous process hiding behind Terminal.\n\n## The Solution: Give Your CLI an Identity\n\n### Step 1: The Magic Info.plist\n\nFirst revelation: CLI tools can have Info.plist files! Here’s the minimal version that makes everything work:\n\n```xml\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n    <key>CFBundleIdentifier</key>\n    <string>com.yourcompany.yourcli</string>\n    <key>CFBundleName</key>\n    <string>YourCLI</string>\n    <key>CFBundleVersion</key>\n    <string>1.0.0</string>\n    <key>NSAppleEventsUsageDescription</key>\n    <string>YourCLI needs to send AppleEvents to control other applications for automation.</string>\n</dict>\n</plist>\n```\n\nThat `NSAppleEventsUsageDescription` is crucial - it’s what users see in the permission dialog. Make it clear and specific.\n\n### Step 2: Embedding the Info.plist (The Secret Sauce)\n\nHere’s where it gets interesting. You need to embed this Info.plist into your binary’s `__TEXT/__info_plist` section. For Swift Package Manager:\n\n```swift\n// Package.swift\n.executableTarget(\n    name: \"yourcli\",\n    dependencies: [/* ... */],\n    linkerSettings: [\n        .unsafeFlags([\n            \"-Xlinker\", \"-sectcreate\",\n            \"-Xlinker\", \"__TEXT\",\n            \"-Xlinker\", \"__info_plist\",\n            \"-Xlinker\", \"Sources/Resources/Info.plist\"\n        ])\n    ]\n)\n```\n\nThis creates a special section in your binary that macOS reads to identify your app. You can verify it worked:\n\n```bash\notool -s __TEXT __info_plist yourcli | xxd -r -p | plutil -p -\n```\n\n### Step 3: Entitlements for the Paranoid OS\n\nCreate an entitlements file - yes, CLI tools can have these too:\n\n```xml\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n    <key>com.apple.security.automation.apple-events</key>\n    <true/>\n</dict>\n</plist>\n```\n\n### Step 4: Code Signing (The Final Boss)\n\nHere’s my battle-tested signing script that handles both development and production:\n\n```bash\n#!/bin/bash\nset -e\n\nBINARY_PATH=\".build/release/yourcli\"\n\n# Check for Developer ID certificate\nif security find-identity -p codesigning -v | grep -q \"Developer ID Application\"; then\n    # Production signing\n    SIGNING_IDENTITY=$(security find-identity -p codesigning -v | \\\n        grep \"Developer ID Application\" | head -1 | awk '{print $2}')\n    \n    codesign --force \\\n        --sign \"$SIGNING_IDENTITY\" \\\n        --options runtime \\\n        --entitlements \"yourcli.entitlements\" \\\n        --identifier \"com.yourcompany.yourcli\" \\\n        --timestamp \\\n        \"$BINARY_PATH\"\nelse\n    # Ad-hoc signing for development\n    codesign --force \\\n        --sign - \\\n        --entitlements \"yourcli.entitlements\" \\\n        --identifier \"com.yourcompany.yourcli\" \\\n        \"$BINARY_PATH\"\nfi\n```\n\n## The Advanced Stuff: Escaping Cursor’s Shadow\n\nRemember how permission dialogs blame the hosting app (e.g. Cursor when you build an MCP) instead of your app? There’s an undocumented API to fix that: `responsibility_spawnattrs_setdisclaim`. I discovered this gem from [Qt’s excellent blog post about the responsible process problem](https://www.qt.io/blog/the-curious-case-of-the-responsible-process).\n\n```swift\n// Bridge the private API\n@_silgen_name(\"responsibility_spawnattrs_setdisclaim\")\nfunc responsibility_spawnattrs_setdisclaim(\n    _ attr: UnsafeMutablePointer<posix_spawnattr_t?>,\n    _ disclaim: Int32\n) -> Int32\n\n// Use it to launch a subprocess that owns its permissions\nfunc launchWithOwnPermissions(path: String, arguments: [String]) throws {\n    var attr: posix_spawnattr_t?\n    posix_spawnattr_init(&attr)\n    defer { posix_spawnattr_destroy(&attr) }\n    \n    // The magic happens here\n    responsibility_spawnattrs_setdisclaim(&attr, 1)\n    \n    var pid: pid_t = 0\n    let argv = ([path] + arguments).map { strdup($0) }\n    defer { argv.forEach { free($0) } }\n    \n    let status = posix_spawn(&pid, path, nil, &attr, argv + [nil], environ)\n    guard status == 0 else {\n        throw POSIXError(POSIXError.Code(rawValue: status) ?? .ENODEV)\n    }\n}\n```\n\nThis makes your CLI tool responsible for its own permissions, not its parent process. The permission dialog will now correctly show “Your CLI wants to control…”\n\n## Testing Your Implementation\n\nReset permissions to test the flow:\n\n```bash\ntccutil reset AppleEvents com.yourcompany.yourcli\n```\n\nCheck your work:\n\n```bash\n# Verify Info.plist embedding\notool -s __TEXT __info_plist yourcli\n\n# Check entitlements\ncodesign -d --entitlements - yourcli\n\n# Verify signature\ncodesign -dv yourcli\n```\n\n## The Bottom Line\n\nMaking AppleScript work in CLI tools requires:\n\n1. An embedded Info.plist for identity\n2. Proper entitlements for permissions\n3. Correct code signing\n4. Optional: `responsibility_spawnattrs_setdisclaim` for cleaner permission dialogs\n\nBuilding native is hard mode. Every day you learn a new insanity. May your permission dialogs always show the right app name.\n",
  "authors": [],
  "captured_at": "2026-09-27T19:29:15.293681927+00:00",
  "category": "programming",
  "content_html": "<p><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://steipete.me/assets/img/2025/applescript-cli-macos-complete-guide/header.png\" alt=\"\" loading=\"eager\" decoding=\"async\" fetchpriority=\"high\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture></p>\n<p><em>Or: How I Learned to Stop Fighting TCC and Embrace the Info.plist</em></p>\n<p><strong>TL;DR:</strong> If you’re building a macOS CLI that uses AppleScript, you need to embed an Info.plist into your binary, sign it with proper entitlements, and optionally use the undocumented <code>responsibility_spawnattrs_setdisclaim</code> API to avoid permission dialogs that blames the hosting app.</p>\n<p>This all started with Cursor being annoying. You know how it goes - you’re in the zone, AI is looping through its tasks, and then bam! The inline terminal opens something blocking (file watcher, dev server, …) and the whole loop stops. I have to manually click around to get things moving again.</p>\n<p>My solution? Build an MCP that controls an external terminal. That way, even when commands block, Cursor’s loop keeps running. I called it <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/Terminator\">Terminator</a> - ‘cause who doesn’t love a good terminal/Terminator pun?</p>\n<h2>The Evolution of a Hack</h2>\n<p>My first attempt was <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/steipete/Terminator/blob/main/scripts/terminator.scpt\">pure AppleScript</a> - simple, direct terminal automation. It worked! Well, sort of… The script needed window focus to function, which meant it would constantly steal focus while Cursor was running.</p>\n<p>The focus-stealing got so bad that at one point, the AI started writing its own AppleScript to detect which app was in the foreground. When it discovered Chrome was blocking its terminal access, it simply… killed all my Chrome windows. That was <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://steipete.me/posts/2025/when-ai-meets-madness-peters-16-hour-days#the-gemini-chrome-massacre\">the first time I apologized to an AI agent</a>. Clearly, I needed a better solution.</p>\n<p>That’s when I fell down the rabbit hole. Getting AppleScript to work in a CLI tool turned out to be a maze of undocumented APIs, security permissions, and macOS quirks that nobody warns you about.</p>\n<p>Sure, I could have used Apple’s <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/osascript\"><code>osascript</code></a> command and called it a day. But where’s the fun in that? Plus, AppKit’s AppleScript API gives you much better error handling - if you can get it working.</p>\n<h2>Plot Twist: Enter Claude Code</h2>\n<p>Here’s the thing - I never actually finished Terminator. Why? Because <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://docs.anthropic.com/en/docs/claude-code\">Claude Code</a> came along and made it obsolete. As a massive Claude Code fanboy (seriously, <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://steipete.me/posts/2025/claude-code-is-my-computer/\">check</a> <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://steipete.me/posts/2025/commanding-your-claude-code-army/\">out</a> <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://steipete.me/posts/command-your-claude-code-army-reloaded/\">my</a> <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://steipete.me/posts/2025/vibe-meter-2-claude-code-usage-calculation/\">posts</a>), I ditched Cursor. Claude Code doesn’t have these inline terminal blocking issues.</p>\n<p>I’m writing this here since future you or me will stumble into the same issue eventually, and hopefully you can just drag this URL into Claude Code and it’ll fix everything up. Let Terminator lurk unfinished in my GitHub.</p>\n<h2>The Problem: Terminal Gets All the Blame</h2>\n<p>Here’s what happens when you naively use <code>NSAppleScript</code> in a CLI tool:</p>\n<pre><code class=\"language-swift\"><span class=\"code-snippet\" data-language=\"Swift\">let script = NSAppleScript(source: &quot;tell application \\&quot;Finder\\&quot; to activate&quot;)\nscript?.executeAndReturnError(nil)  // Spoiler: This won&#39;t work as expected\n</span></code></pre>\n<p>You’ll either get:</p>\n<ol>\n<li>Silent failure (no error, no result, no nothing)</li>\n<li>A permission dialog that says “Terminal wants to control Finder/Cursor” (not your cli)</li>\n<li>Error -1750 (errOSASystemError) with zero helpful context</li>\n</ol>\n<p>The root cause? macOS’s security model requires proper app identification through bundle IDs, code signing, and entitlements. Without these, your CLI tool is just an anonymous process hiding behind Terminal.</p>\n<h2>The Solution: Give Your CLI an Identity</h2>\n<h3>Step 1: The Magic Info.plist</h3>\n<p>First revelation: CLI tools can have Info.plist files! Here’s the minimal version that makes everything work:</p>\n<pre><code class=\"language-xml\"><span class=\"code-snippet\" data-language=\"XML\"><span class=\"syntax-text syntax-xml\"><span class=\"syntax-meta syntax-tag syntax-preprocessor syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;?</span><span class=\"syntax-entity syntax-name syntax-tag syntax-xml\">xml</span> <span class=\"syntax-entity syntax-other syntax-attribute-name syntax-localname syntax-xml\">version</span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-xml\">=</span><span class=\"syntax-string syntax-quoted syntax-double syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-xml\">&quot;</span>1.0<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-xml\">&quot;</span></span> <span class=\"syntax-entity syntax-other syntax-attribute-name syntax-localname syntax-xml\">encoding</span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-xml\">=</span><span class=\"syntax-string syntax-quoted syntax-double syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-xml\">&quot;</span>UTF-8<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-xml\">&quot;</span></span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">?&gt;</span></span>\n<span class=\"syntax-meta syntax-tag syntax-sgml syntax-doctype syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;!</span><span class=\"syntax-keyword syntax-doctype syntax-xml\">DOCTYPE</span> <span class=\"syntax-variable syntax-documentroot syntax-xml\">plist</span> PUBLIC &quot;-//Apple//DTD PLIST 1.0//EN&quot; &quot;http://www.apple.com/DTDs/PropertyList-1.0.dtd&quot;<span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">&gt;</span></span>\n<span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">plist</span> <span class=\"syntax-entity syntax-other syntax-attribute-name syntax-localname syntax-xml\">version</span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-xml\">=</span><span class=\"syntax-string syntax-quoted syntax-double syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-xml\">&quot;</span>1.0<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-xml\">&quot;</span></span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">&gt;</span></span>\n<span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">dict</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">&gt;</span></span>\n    <span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">key</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">&gt;</span></span>CFBundleIdentifier<span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;/</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">key</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">&gt;</span></span>\n    <span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">string</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">&gt;</span></span>com.yourcompany.yourcli<span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;/</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">string</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">&gt;</span></span>\n    <span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">key</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">&gt;</span></span>CFBundleName<span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;/</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">key</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">&gt;</span></span>\n    <span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">string</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">&gt;</span></span>YourCLI<span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;/</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">string</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">&gt;</span></span>\n    <span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">key</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">&gt;</span></span>CFBundleVersion<span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;/</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">key</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">&gt;</span></span>\n    <span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">string</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">&gt;</span></span>1.0.0<span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;/</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">string</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">&gt;</span></span>\n    <span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">key</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">&gt;</span></span>NSAppleEventsUsageDescription<span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;/</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">key</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">&gt;</span></span>\n    <span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">string</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">&gt;</span></span>YourCLI needs to send AppleEvents to control other applications for automation.<span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;/</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">string</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">&gt;</span></span>\n<span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;/</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">dict</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">&gt;</span></span>\n<span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;/</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">plist</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">&gt;</span></span>\n</span></span></code></pre>\n<p>That <code>NSAppleEventsUsageDescription</code> is crucial - it’s what users see in the permission dialog. Make it clear and specific.</p>\n<h3>Step 2: Embedding the Info.plist (The Secret Sauce)</h3>\n<p>Here’s where it gets interesting. You need to embed this Info.plist into your binary’s <code>__TEXT/__info_plist</code> section. For Swift Package Manager:</p>\n<pre><code class=\"language-swift\"><span class=\"code-snippet\" data-language=\"Swift\">// Package.swift\n.executableTarget(\n    name: &quot;yourcli&quot;,\n    dependencies: [/* ... */],\n    linkerSettings: [\n        .unsafeFlags([\n            &quot;-Xlinker&quot;, &quot;-sectcreate&quot;,\n            &quot;-Xlinker&quot;, &quot;__TEXT&quot;,\n            &quot;-Xlinker&quot;, &quot;__info_plist&quot;,\n            &quot;-Xlinker&quot;, &quot;Sources/Resources/Info.plist&quot;\n        ])\n    ]\n)\n</span></code></pre>\n<p>This creates a special section in your binary that macOS reads to identify your app. You can verify it worked:</p>\n<pre><code class=\"language-bash\"><span class=\"code-snippet\" data-language=\"Shell\"><span class=\"syntax-source syntax-shell syntax-bash\"><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">otool</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> -</span>s</span> __TEXT __info_plist yourcli</span> <span class=\"syntax-keyword syntax-operator syntax-logical syntax-pipe syntax-shell\">|</span> <span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">xxd</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> -</span>r</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> -</span>p</span></span> <span class=\"syntax-keyword syntax-operator syntax-logical syntax-pipe syntax-shell\">|</span> <span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">plutil</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> -</span>p</span> -</span>\n</span></span></code></pre>\n<h3>Step 3: Entitlements for the Paranoid OS</h3>\n<p>Create an entitlements file - yes, CLI tools can have these too:</p>\n<pre><code class=\"language-xml\"><span class=\"code-snippet\" data-language=\"XML\"><span class=\"syntax-text syntax-xml\"><span class=\"syntax-meta syntax-tag syntax-preprocessor syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;?</span><span class=\"syntax-entity syntax-name syntax-tag syntax-xml\">xml</span> <span class=\"syntax-entity syntax-other syntax-attribute-name syntax-localname syntax-xml\">version</span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-xml\">=</span><span class=\"syntax-string syntax-quoted syntax-double syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-xml\">&quot;</span>1.0<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-xml\">&quot;</span></span> <span class=\"syntax-entity syntax-other syntax-attribute-name syntax-localname syntax-xml\">encoding</span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-xml\">=</span><span class=\"syntax-string syntax-quoted syntax-double syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-xml\">&quot;</span>UTF-8<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-xml\">&quot;</span></span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">?&gt;</span></span>\n<span class=\"syntax-meta syntax-tag syntax-sgml syntax-doctype syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;!</span><span class=\"syntax-keyword syntax-doctype syntax-xml\">DOCTYPE</span> <span class=\"syntax-variable syntax-documentroot syntax-xml\">plist</span> PUBLIC &quot;-//Apple//DTD PLIST 1.0//EN&quot; &quot;http://www.apple.com/DTDs/PropertyList-1.0.dtd&quot;<span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">&gt;</span></span>\n<span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">plist</span> <span class=\"syntax-entity syntax-other syntax-attribute-name syntax-localname syntax-xml\">version</span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-xml\">=</span><span class=\"syntax-string syntax-quoted syntax-double syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-xml\">&quot;</span>1.0<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-xml\">&quot;</span></span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">&gt;</span></span>\n<span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">dict</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">&gt;</span></span>\n    <span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">key</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">&gt;</span></span>com.apple.security.automation.apple-events<span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;/</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">key</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">&gt;</span></span>\n    <span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">true</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">/&gt;</span></span>\n<span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;/</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">dict</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">&gt;</span></span>\n<span class=\"syntax-meta syntax-tag syntax-xml\"><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-begin syntax-xml\">&lt;/</span><span class=\"syntax-entity syntax-name syntax-tag syntax-localname syntax-xml\">plist</span><span class=\"syntax-punctuation syntax-definition syntax-tag syntax-end syntax-xml\">&gt;</span></span>\n</span></span></code></pre>\n<h3>Step 4: Code Signing (The Final Boss)</h3>\n<p>Here’s my battle-tested signing script that handles both development and production:</p>\n<pre><code class=\"language-bash\"><span class=\"code-snippet\" data-language=\"Shell\"><span class=\"syntax-source syntax-shell syntax-bash\"><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell\">#</span></span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\">!/bin/bash</span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\">\n</span><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-support syntax-function syntax-set syntax-shell\">set</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> <span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">-</span>e</span></span>\n\n<span class=\"syntax-variable syntax-other syntax-readwrite syntax-assignment syntax-shell\">BINARY_PATH</span><span class=\"syntax-keyword syntax-operator syntax-assignment syntax-shell\">=</span><span class=\"syntax-string syntax-unquoted syntax-shell\"><span class=\"syntax-string syntax-quoted syntax-double syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&quot;</span>.build/release/yourcli<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&quot;</span></span></span>\n\n<span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell\">#</span></span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"> Check for Developer ID certificate</span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\">\n</span><span class=\"syntax-keyword syntax-control syntax-if syntax-begin syntax-shell\">if</span> <span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">security</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> find-identity<span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> -</span>p</span> codesigning<span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> -</span>v</span></span> <span class=\"syntax-keyword syntax-operator syntax-logical syntax-pipe syntax-shell\">|</span> <span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">grep</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> -</span>q</span> <span class=\"syntax-string syntax-quoted syntax-double syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&quot;</span>Developer ID Application<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&quot;</span></span></span><span class=\"syntax-keyword syntax-operator syntax-logical syntax-continue syntax-shell\">;</span> <span class=\"syntax-keyword syntax-control syntax-if syntax-then syntax-shell\">then</span>\n    <span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell\">#</span></span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"> Production signing</span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\">\n</span>    <span class=\"syntax-variable syntax-other syntax-readwrite syntax-assignment syntax-shell\">SIGNING_IDENTITY</span><span class=\"syntax-keyword syntax-operator syntax-assignment syntax-shell\">=</span><span class=\"syntax-string syntax-unquoted syntax-shell\"><span class=\"syntax-meta syntax-group syntax-expansion syntax-command syntax-parens syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-variable syntax-shell\">$</span><span class=\"syntax-punctuation syntax-section syntax-parens syntax-begin syntax-shell\">(</span><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">security</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> find-identity<span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> -</span>p</span> codesigning<span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> -</span>v</span></span> <span class=\"syntax-keyword syntax-operator syntax-logical syntax-pipe syntax-shell\">|</span> <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span>        <span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">grep</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> <span class=\"syntax-string syntax-quoted syntax-double syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&quot;</span>Developer ID Application<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&quot;</span></span></span> <span class=\"syntax-keyword syntax-operator syntax-logical syntax-pipe syntax-shell\">|</span> <span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">head</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> -</span>1</span></span> <span class=\"syntax-keyword syntax-operator syntax-logical syntax-pipe syntax-shell\">|</span> <span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">awk</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> <span class=\"syntax-string syntax-quoted syntax-single syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&#39;</span>{print $2}<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&#39;</span></span></span><span class=\"syntax-punctuation syntax-section syntax-parens syntax-end syntax-shell\">)</span></span></span>\n    \n    <span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">codesign</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> --</span>force</span> <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">        --</span>sign</span> <span class=\"syntax-string syntax-quoted syntax-double syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&quot;</span><span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-variable syntax-shell\">$</span><span class=\"syntax-variable syntax-other syntax-readwrite syntax-shell\">SIGNING_IDENTITY</span></span><span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&quot;</span></span> <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">        --</span>options</span> runtime <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">        --</span>entitlements</span> <span class=\"syntax-string syntax-quoted syntax-double syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&quot;</span>yourcli.entitlements<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&quot;</span></span> <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">        --</span>identifier</span> <span class=\"syntax-string syntax-quoted syntax-double syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&quot;</span>com.yourcompany.yourcli<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&quot;</span></span> <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">        --</span>timestamp</span> <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span>        <span class=\"syntax-string syntax-quoted syntax-double syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&quot;</span><span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-variable syntax-shell\">$</span><span class=\"syntax-variable syntax-other syntax-readwrite syntax-shell\">BINARY_PATH</span></span><span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&quot;</span></span></span>\n<span class=\"syntax-keyword syntax-control syntax-if syntax-else syntax-shell\">else</span>\n    <span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell\">#</span></span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"> Ad-hoc signing for development</span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\">\n</span>    <span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">codesign</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> --</span>force</span> <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">        --</span>sign</span> - <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">        --</span>entitlements</span> <span class=\"syntax-string syntax-quoted syntax-double syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&quot;</span>yourcli.entitlements<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&quot;</span></span> <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">        --</span>identifier</span> <span class=\"syntax-string syntax-quoted syntax-double syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&quot;</span>com.yourcompany.yourcli<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&quot;</span></span> <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span>        <span class=\"syntax-string syntax-quoted syntax-double syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&quot;</span><span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-variable syntax-shell\">$</span><span class=\"syntax-variable syntax-other syntax-readwrite syntax-shell\">BINARY_PATH</span></span><span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&quot;</span></span></span>\n<span class=\"syntax-keyword syntax-control syntax-if syntax-end syntax-shell\">fi</span>\n</span></span></code></pre>\n<h2>The Advanced Stuff: Escaping Cursor’s Shadow</h2>\n<p>Remember how permission dialogs blame the hosting app (e.g. Cursor when you build an MCP) instead of your app? There’s an undocumented API to fix that: <code>responsibility_spawnattrs_setdisclaim</code>. I discovered this gem from <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://www.qt.io/blog/the-curious-case-of-the-responsible-process\">Qt’s excellent blog post about the responsible process problem</a>.</p>\n<pre><code class=\"language-swift\"><span class=\"code-snippet\" data-language=\"Swift\">// Bridge the private API\n@_silgen_name(&quot;responsibility_spawnattrs_setdisclaim&quot;)\nfunc responsibility_spawnattrs_setdisclaim(\n    _ attr: UnsafeMutablePointer&lt;posix_spawnattr_t?&gt;,\n    _ disclaim: Int32\n) -&gt; Int32\n\n// Use it to launch a subprocess that owns its permissions\nfunc launchWithOwnPermissions(path: String, arguments: [String]) throws {\n    var attr: posix_spawnattr_t?\n    posix_spawnattr_init(&amp;attr)\n    defer { posix_spawnattr_destroy(&amp;attr) }\n    \n    // The magic happens here\n    responsibility_spawnattrs_setdisclaim(&amp;attr, 1)\n    \n    var pid: pid_t = 0\n    let argv = ([path] + arguments).map { strdup($0) }\n    defer { argv.forEach { free($0) } }\n    \n    let status = posix_spawn(&amp;pid, path, nil, &amp;attr, argv + [nil], environ)\n    guard status == 0 else {\n        throw POSIXError(POSIXError.Code(rawValue: status) ?? .ENODEV)\n    }\n}\n</span></code></pre>\n<p>This makes your CLI tool responsible for its own permissions, not its parent process. The permission dialog will now correctly show “Your CLI wants to control…”</p>\n<h2>Testing Your Implementation</h2>\n<p>Reset permissions to test the flow:</p>\n<pre><code class=\"language-bash\"><span class=\"code-snippet\" data-language=\"Shell\"><span class=\"syntax-source syntax-shell syntax-bash\"><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">tccutil</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> reset AppleEvents com.yourcompany.yourcli</span>\n</span></span></code></pre>\n<p>Check your work:</p>\n<pre><code class=\"language-bash\"><span class=\"code-snippet\" data-language=\"Shell\"><span class=\"syntax-source syntax-shell syntax-bash\"><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell\">#</span></span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"> Verify Info.plist embedding</span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\">\n</span><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">otool</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> -</span>s</span> __TEXT __info_plist yourcli</span>\n\n<span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell\">#</span></span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"> Check entitlements</span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\">\n</span><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">codesign</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> -</span>d</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> --</span>entitlements</span> - yourcli</span>\n\n<span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-begin syntax-shell\">#</span></span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\"> Verify signature</span><span class=\"syntax-comment syntax-line syntax-number-sign syntax-shell\">\n</span><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">codesign</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> -</span>dv</span> yourcli</span>\n</span></span></code></pre>\n<h2>The Bottom Line</h2>\n<p>Making AppleScript work in CLI tools requires:</p>\n<ol>\n<li>An embedded Info.plist for identity</li>\n<li>Proper entitlements for permissions</li>\n<li>Correct code signing</li>\n<li>Optional: <code>responsibility_spawnattrs_setdisclaim</code> for cleaner permission dialogs</li>\n</ol>\n<p>Building native is hard mode. Every day you learn a new insanity. May your permission dialogs always show the right app name.</p>\n",
  "content_markdown": "![](https://steipete.me/assets/img/2025/applescript-cli-macos-complete-guide/header.png)\n\n*Or: How I Learned to Stop Fighting TCC and Embrace the Info.plist*\n\n**TL;DR:** If you’re building a macOS CLI that uses AppleScript, you need to embed an Info.plist into your binary, sign it with proper entitlements, and optionally use the undocumented `responsibility_spawnattrs_setdisclaim` API to avoid permission dialogs that blames the hosting app.\n\nThis all started with Cursor being annoying. You know how it goes - you’re in the zone, AI is looping through its tasks, and then bam! The inline terminal opens something blocking (file watcher, dev server, …) and the whole loop stops. I have to manually click around to get things moving again.\n\nMy solution? Build an MCP that controls an external terminal. That way, even when commands block, Cursor’s loop keeps running. I called it [Terminator](https://github.com/steipete/Terminator) - ‘cause who doesn’t love a good terminal/Terminator pun?\n\n## The Evolution of a Hack\n\nMy first attempt was [pure AppleScript](https://github.com/steipete/Terminator/blob/main/scripts/terminator.scpt) - simple, direct terminal automation. It worked! Well, sort of… The script needed window focus to function, which meant it would constantly steal focus while Cursor was running.\n\nThe focus-stealing got so bad that at one point, the AI started writing its own AppleScript to detect which app was in the foreground. When it discovered Chrome was blocking its terminal access, it simply… killed all my Chrome windows. That was [the first time I apologized to an AI agent](https://steipete.me/posts/2025/when-ai-meets-madness-peters-16-hour-days#the-gemini-chrome-massacre). Clearly, I needed a better solution.\n\nThat’s when I fell down the rabbit hole. Getting AppleScript to work in a CLI tool turned out to be a maze of undocumented APIs, security permissions, and macOS quirks that nobody warns you about.\n\nSure, I could have used Apple’s [`osascript`](https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/osascript) command and called it a day. But where’s the fun in that? Plus, AppKit’s AppleScript API gives you much better error handling - if you can get it working.\n\n## Plot Twist: Enter Claude Code\n\nHere’s the thing - I never actually finished Terminator. Why? Because [Claude Code](https://docs.anthropic.com/en/docs/claude-code) came along and made it obsolete. As a massive Claude Code fanboy (seriously, [check](https://steipete.me/posts/2025/claude-code-is-my-computer/) [out](https://steipete.me/posts/2025/commanding-your-claude-code-army/) [my](https://steipete.me/posts/command-your-claude-code-army-reloaded/) [posts](https://steipete.me/posts/2025/vibe-meter-2-claude-code-usage-calculation/)), I ditched Cursor. Claude Code doesn’t have these inline terminal blocking issues.\n\nI’m writing this here since future you or me will stumble into the same issue eventually, and hopefully you can just drag this URL into Claude Code and it’ll fix everything up. Let Terminator lurk unfinished in my GitHub.\n\n## The Problem: Terminal Gets All the Blame\n\nHere’s what happens when you naively use `NSAppleScript` in a CLI tool:\n\n```swift\nlet script = NSAppleScript(source: \"tell application \\\"Finder\\\" to activate\")\nscript?.executeAndReturnError(nil)  // Spoiler: This won't work as expected\n```\n\nYou’ll either get:\n\n1. Silent failure (no error, no result, no nothing)\n2. A permission dialog that says “Terminal wants to control Finder/Cursor” (not your cli)\n3. Error -1750 (errOSASystemError) with zero helpful context\n\nThe root cause? macOS’s security model requires proper app identification through bundle IDs, code signing, and entitlements. Without these, your CLI tool is just an anonymous process hiding behind Terminal.\n\n## The Solution: Give Your CLI an Identity\n\n### Step 1: The Magic Info.plist\n\nFirst revelation: CLI tools can have Info.plist files! Here’s the minimal version that makes everything work:\n\n```xml\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n    <key>CFBundleIdentifier</key>\n    <string>com.yourcompany.yourcli</string>\n    <key>CFBundleName</key>\n    <string>YourCLI</string>\n    <key>CFBundleVersion</key>\n    <string>1.0.0</string>\n    <key>NSAppleEventsUsageDescription</key>\n    <string>YourCLI needs to send AppleEvents to control other applications for automation.</string>\n</dict>\n</plist>\n```\n\nThat `NSAppleEventsUsageDescription` is crucial - it’s what users see in the permission dialog. Make it clear and specific.\n\n### Step 2: Embedding the Info.plist (The Secret Sauce)\n\nHere’s where it gets interesting. You need to embed this Info.plist into your binary’s `__TEXT/__info_plist` section. For Swift Package Manager:\n\n```swift\n// Package.swift\n.executableTarget(\n    name: \"yourcli\",\n    dependencies: [/* ... */],\n    linkerSettings: [\n        .unsafeFlags([\n            \"-Xlinker\", \"-sectcreate\",\n            \"-Xlinker\", \"__TEXT\",\n            \"-Xlinker\", \"__info_plist\",\n            \"-Xlinker\", \"Sources/Resources/Info.plist\"\n        ])\n    ]\n)\n```\n\nThis creates a special section in your binary that macOS reads to identify your app. You can verify it worked:\n\n```bash\notool -s __TEXT __info_plist yourcli | xxd -r -p | plutil -p -\n```\n\n### Step 3: Entitlements for the Paranoid OS\n\nCreate an entitlements file - yes, CLI tools can have these too:\n\n```xml\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n    <key>com.apple.security.automation.apple-events</key>\n    <true/>\n</dict>\n</plist>\n```\n\n### Step 4: Code Signing (The Final Boss)\n\nHere’s my battle-tested signing script that handles both development and production:\n\n```bash\n#!/bin/bash\nset -e\n\nBINARY_PATH=\".build/release/yourcli\"\n\n# Check for Developer ID certificate\nif security find-identity -p codesigning -v | grep -q \"Developer ID Application\"; then\n    # Production signing\n    SIGNING_IDENTITY=$(security find-identity -p codesigning -v | \\\n        grep \"Developer ID Application\" | head -1 | awk '{print $2}')\n    \n    codesign --force \\\n        --sign \"$SIGNING_IDENTITY\" \\\n        --options runtime \\\n        --entitlements \"yourcli.entitlements\" \\\n        --identifier \"com.yourcompany.yourcli\" \\\n        --timestamp \\\n        \"$BINARY_PATH\"\nelse\n    # Ad-hoc signing for development\n    codesign --force \\\n        --sign - \\\n        --entitlements \"yourcli.entitlements\" \\\n        --identifier \"com.yourcompany.yourcli\" \\\n        \"$BINARY_PATH\"\nfi\n```\n\n## The Advanced Stuff: Escaping Cursor’s Shadow\n\nRemember how permission dialogs blame the hosting app (e.g. Cursor when you build an MCP) instead of your app? There’s an undocumented API to fix that: `responsibility_spawnattrs_setdisclaim`. I discovered this gem from [Qt’s excellent blog post about the responsible process problem](https://www.qt.io/blog/the-curious-case-of-the-responsible-process).\n\n```swift\n// Bridge the private API\n@_silgen_name(\"responsibility_spawnattrs_setdisclaim\")\nfunc responsibility_spawnattrs_setdisclaim(\n    _ attr: UnsafeMutablePointer<posix_spawnattr_t?>,\n    _ disclaim: Int32\n) -> Int32\n\n// Use it to launch a subprocess that owns its permissions\nfunc launchWithOwnPermissions(path: String, arguments: [String]) throws {\n    var attr: posix_spawnattr_t?\n    posix_spawnattr_init(&attr)\n    defer { posix_spawnattr_destroy(&attr) }\n    \n    // The magic happens here\n    responsibility_spawnattrs_setdisclaim(&attr, 1)\n    \n    var pid: pid_t = 0\n    let argv = ([path] + arguments).map { strdup($0) }\n    defer { argv.forEach { free($0) } }\n    \n    let status = posix_spawn(&pid, path, nil, &attr, argv + [nil], environ)\n    guard status == 0 else {\n        throw POSIXError(POSIXError.Code(rawValue: status) ?? .ENODEV)\n    }\n}\n```\n\nThis makes your CLI tool responsible for its own permissions, not its parent process. The permission dialog will now correctly show “Your CLI wants to control…”\n\n## Testing Your Implementation\n\nReset permissions to test the flow:\n\n```bash\ntccutil reset AppleEvents com.yourcompany.yourcli\n```\n\nCheck your work:\n\n```bash\n# Verify Info.plist embedding\notool -s __TEXT __info_plist yourcli\n\n# Check entitlements\ncodesign -d --entitlements - yourcli\n\n# Verify signature\ncodesign -dv yourcli\n```\n\n## The Bottom Line\n\nMaking AppleScript work in CLI tools requires:\n\n1. An embedded Info.plist for identity\n2. Proper entitlements for permissions\n3. Correct code signing\n4. Optional: `responsibility_spawnattrs_setdisclaim` for cleaner permission dialogs\n\nBuilding native is hard mode. Every day you learn a new insanity. May your permission dialogs always show the right app name.\n",
  "dateCreated": "2026-09-27T19:29:15.293681927+00:00",
  "date_created": "2025-07-03T13:00:00+00:00",
  "date_modified": "2025-07-03T13:00:00+00:00",
  "date_published": "2025-07-03T13:00:00+00:00",
  "description": "How to make AppleScript work in macOS CLI tools without permission dialogs blaming Terminal. A deep dive into Info.plist embedding, TCC, and undocumented APIs born from building Terminator MCP.",
  "external_url": "https://steipete.me/posts/2025/applescript-cli-macos-complete-guide/",
  "git": {
    "commit": "ebb0941c5c06a2ab2377342d607535262a3e969e",
    "path": "items/steipete-me/2025/07/2025-07-03-making-applescript-work-in-macos-cli-tools-the-undocumented.md",
    "permalink": "https://github.com/aymericbeaumet/aggr-instance/blob/ebb0941c5c06a2ab2377342d607535262a3e969e/items/steipete-me/2025/07/2025-07-03-making-applescript-work-in-macos-cli-tools-the-undocumented.md"
  },
  "id": "urn:aggr:item:2f01bd13d05d87b1f9bbd30dabc87ffafdaf9c92",
  "isBasedOn": {
    "@id": "https://steipete.me/posts/2025/applescript-cli-macos-complete-guide/"
  },
  "mainEntity": {
    "@id": "https://steipete.me/posts/2025/applescript-cli-macos-complete-guide/",
    "@type": "CreativeWork",
    "archivedAt": {
      "@id": "https://aggr.aymericbeaumet.com/items/steipete-me/2025-07-03-making-applescript-work-in-macos-cli-tools-the-undocumented/#webpage"
    },
    "dateModified": "2025-07-03T13:00:00+00:00",
    "datePublished": "2025-07-03T13:00:00+00:00",
    "headline": "Making AppleScript Work in macOS CLI Tools: The Undocumented Parts",
    "timeRequired": "PT5M",
    "url": "https://steipete.me/posts/2025/applescript-cli-macos-complete-guide/",
    "wordCount": 1046
  },
  "name": "Archived snapshot: Making AppleScript Work in macOS CLI Tools: The Undocumented Parts",
  "preview": null,
  "reading_minutes": 5,
  "replicated_at": null,
  "source": "steipete-me",
  "source_name": "Peter Steinberger",
  "summary": "How to make AppleScript work in macOS CLI tools without permission dialogs blaming Terminal. A deep dive into Info.plist embedding, TCC, and undocumented APIs born from building Terminator MCP.",
  "tags": [],
  "temporalCoverage": "2026-09-27T19:29:15.293681927+00:00",
  "title": "Making AppleScript Work in macOS CLI Tools: The Undocumented Parts",
  "url": "https://aggr.aymericbeaumet.com/items/steipete-me/2025-07-03-making-applescript-work-in-macos-cli-tools-the-undocumented/",
  "word_count": 1046
}