惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - 三生石上(FineUI控件)
L
LangChain Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
Blog — PlanetScale
Blog — PlanetScale
H
Hackread – Cybersecurity News, Data Breaches, AI and More
D
Docker
T
Tailwind CSS Blog
T
The Blog of Author Tim Ferriss
U
Unit 42
B
Blog
N
Netflix TechBlog - Medium
T
Threat Research - Cisco Blogs
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
O
OpenAI News
M
MIT News - Artificial intelligence
D
DataBreaches.Net
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
L
LINUX DO - 热门话题
C
CERT Recently Published Vulnerability Notes
V
Visual Studio Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
N
News and Events Feed by Topic
Vercel News
Vercel News
T
Tenable Blog
Security Latest
Security Latest
C
Check Point Blog
云风的 BLOG
云风的 BLOG
PCI Perspectives
PCI Perspectives
月光博客
月光博客
TaoSecurity Blog
TaoSecurity Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Project Zero
Project Zero
雷峰网
雷峰网
IT之家
IT之家
H
Hacker News: Front Page
Microsoft Security Blog
Microsoft Security Blog
B
Blog RSS Feed
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Last Week in AI
Last Week in AI
G
Google Developers Blog
Forbes - Security
Forbes - Security
The Register - Security
The Register - Security
Cyberwarzone
Cyberwarzone
小众软件
小众软件
Martin Fowler
Martin Fowler
K
Kaspersky official blog
P
Proofpoint News Feed
T
Threatpost
Google Online Security Blog
Google Online Security Blog
Microsoft Azure Blog
Microsoft Azure Blog

Peter Steinberger

OpenClaw, OpenAI and the future | Peter Steinberger Shipping at Inference-Speed | Peter Steinberger The Signature Flicker | Peter Steinberger Just Talk To It - the no-bs Way of Agentic Engineering | Peter Steinberger Claude Code Anonymous | Peter Steinberger Live Coding Session: Building Arena | Peter Steinberger My Current AI Dev Workflow | Peter Steinberger Essential Reading for Agentic Engineers - August 2025 | Peter Steinberger Just One More Prompt | Peter Steinberger Poltergeist: The Ghost That Keeps Your Builds Fresh | Peter Steinberger Don't read this Startup Slop | Peter Steinberger Essential Reading for Agentic Engineers - July 2025 | Peter Steinberger Self-Hosting AI Models After Claude's Usage Limits | Peter Steinberger Logging Privacy Shenanigans | Peter Steinberger VibeTunnel's first AI-anniversary | Peter Steinberger Peekaboo 2.0 – Free the CLI from its MCP shackles | Peter Steinberger Command your Claude Code Army, Reloaded | Peter Steinberger Essential Reading for Agentic Engineers | Peter Steinberger Slot Machines for Programmers: How Peter Builds Apps 20x Faster with AI | Peter Steinberger My AI Workflow for Understanding Any Codebase | Peter Steinberger stats.store: Privacy-First Sparkle Analytics | Peter Steinberger Showing Settings from macOS Menu Bar Items: A 5-Hour Journey | Peter Steinberger VibeTunnel: Turn Any Browser into Your Mac's Terminal | Peter Steinberger Vibe Meter 2.0: Calculating Claude Code Usage with Token Counting | Peter Steinberger llm.codes: Make Apple Docs AI-Readable | Peter Steinberger Automatic Observation Tracking in UIKit and AppKit: The Feature Apple Forgot to Mention | Peter Steinberger Peekaboo MCP – lightning-fast macOS screenshots for AI agents | Peter Steinberger Migrating 700+ Tests to Swift Testing: A Real-World Experience | Peter Steinberger Commanding Your Claude Code Army | Peter Steinberger Code Signing and Notarization: Sparkle and Tears | Peter Steinberger Vibe Meter: Monitor Your AI Costs | Peter Steinberger Claude Code is My Computer | Peter Steinberger Stop Over-thinking AI Subscriptions | Peter Steinberger Introducing Demark: HTML in. MD out. Blink-fast. | Peter Steinberger The Future of Vibe Coding: Building with AI, Live and Unfiltered | Peter Steinberger MCP Best Practices | Peter Steinberger Finding My Spark Again | Peter Steinberger Top-Level Menu Visibility in SwiftUI for macOS | Peter Steinberger Fixing keyboardShortcut in SwiftUI | Peter Steinberger Supporting Both Tap and Long Press on a Button in SwiftUI | Peter Steinberger On Using Apple Silicon Mac Mini for Continuous Integration | Peter Steinberger Apple Silicon M1: A Developer's Perspective | Peter Steinberger Gardening Your Twitter: Curating Your Timeline | Peter Steinberger Gardening Your Twitter: Growing Your Followers | Peter Steinberger Forbidden Controls in Catalyst: Optimize Interface for Mac | Peter Steinberger Disabling Keyboard Avoidance in SwiftUI's UIHostingController | Peter Steinberger The State of SwiftUI | Peter Steinberger Logging in Swift | Peter Steinberger Building with Swift Trunk Development Snapshots | Peter Steinberger Calling Super at Runtime in Swift | Peter Steinberger zld — A Faster Version of Apple's Linker | Peter Steinberger How to Fix LLDB: Couldn't IRGen Expression | Peter Steinberger Updating macOS on a Hackintosh | Peter Steinberger InterposeKit — Elegant Swizzling in Swift | Peter Steinberger The Great Mac Catalyst Text Input Crash Hunt | Peter Steinberger Jailbreaking for iOS Developers | Peter Steinberger Network Kernel Core Dump | Peter Steinberger How to macOS Core Dump | Peter Steinberger Kernel Panics and Surprise boot-args | Peter Steinberger The LG UltraFine 5K, kernel_task, and Me | Peter Steinberger Let's Try This Again | Peter Steinberger How We Work at PSPDFKit | Peter Steinberger Swizzling in Swift | Peter Steinberger WWDC for First-Timers, 2019 Edition | Peter Steinberger Challenges of Adopting Drag and Drop | Peter Steinberger Marzipan: Porting iOS Apps to the Mac | Peter Steinberger How to Use Slack and Not Go Crazy | Peter Steinberger Hardcore Debugging - Heavy Weapons for Hard Bugs | Peter Steinberger Binary Frameworks in Swift | Peter Steinberger Even Swiftier Objective-C | Peter Steinberger The Case for Deprecating UITableView | Peter Steinberger Running tests with Clang Address Sanitizer | Peter Steinberger UI testing on iOS, without busy waiting | Peter Steinberger Hiring a distributed team | Peter Steinberger Writing Good Bug Reports | Peter Steinberger Real-time collaboration, Apple, and you | Peter Steinberger Converting Xcode Test Runs to JUnit, the Fast Way | Peter Steinberger Efficient iOS Version Checking | Peter Steinberger Investigating Thread Safety of UIImage | Peter Steinberger Swifty Objective-C | Peter Steinberger Running UI Tests on iOS With Ludicrous Speed | Peter Steinberger A Pragmatic Approach to Cross-Platform | Peter Steinberger Surprises with Swift Extensions | Peter Steinberger Using ccache for Fun and Profit | Peter Steinberger UITableViewController designated initializer woes | Peter Steinberger Researching ResearchKit | Peter Steinberger The curious case of rotation with multiple windows on iOS 8 | Peter Steinberger UIKit Debug Mode | Peter Steinberger Retrofitting containsString: on iOS 7 | Peter Steinberger A Story About Swizzling "the Right Way™" and Touch Forwarding | Peter Steinberger Hacking with Aspects | Peter Steinberger Fixing UITextView On iOS 7 | Peter Steinberger Fixing What Apple Doesn't | Peter Steinberger How To Inspect The View Hierarchy Of Third-Party Apps | Peter Steinberger Fixing UISearchDisplayController On iOS 7 | Peter Steinberger Smart Proxy Delegation | Peter Steinberger Adding Keyboard Shortcuts To UIAlertView | Peter Steinberger How To Center Content Within UIScrollView | Peter Steinberger UIAppearance for Custom Views | Peter Steinberger Hacking Block Support Into UIMenuItem | Peter Steinberger
Making AppleScript Work in macOS CLI Tools: The Undocumented Parts | Peter Steinberger
Peter Steinberger · 2025-07-03 · via Peter Steinberger

Or: How I Learned to Stop Fighting TCC and Embrace the Info.plist

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.

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.

My solution? Build an MCP that controls an external terminal. That way, even when commands block, Cursor’s loop keeps running. I called it Terminator - ‘cause who doesn’t love a good terminal/Terminator pun?

The Evolution of a Hack

My first attempt was pure AppleScript - 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.

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 the first time I apologized to an AI agent. Clearly, I needed a better solution.

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.

Sure, I could have used Apple’s 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.

Plot Twist: Enter Claude Code

Here’s the thing - I never actually finished Terminator. Why? Because Claude Code came along and made it obsolete. As a massive Claude Code fanboy (seriously, check out my posts), I ditched Cursor. Claude Code doesn’t have these inline terminal blocking issues.

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.

The Problem: Terminal Gets All the Blame

Here’s what happens when you naively use NSAppleScript in a CLI tool:

let script = NSAppleScript(source: "tell application \"Finder\" to activate")
script?.executeAndReturnError(nil)  // Spoiler: This won't work as expected

You’ll either get:

  1. Silent failure (no error, no result, no nothing)
  2. A permission dialog that says “Terminal wants to control Finder/Cursor” (not your cli)
  3. Error -1750 (errOSASystemError) with zero helpful context

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.

The Solution: Give Your CLI an Identity

Step 1: The Magic Info.plist

First revelation: CLI tools can have Info.plist files! Here’s the minimal version that makes everything work:

<?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>CFBundleIdentifier</key>
    <string>com.yourcompany.yourcli</string>
    <key>CFBundleName</key>
    <string>YourCLI</string>
    <key>CFBundleVersion</key>
    <string>1.0.0</string>
    <key>NSAppleEventsUsageDescription</key>
    <string>YourCLI needs to send AppleEvents to control other applications for automation.</string>
</dict>
</plist>

That NSAppleEventsUsageDescription is crucial - it’s what users see in the permission dialog. Make it clear and specific.

Step 2: Embedding the Info.plist (The Secret Sauce)

Here’s where it gets interesting. You need to embed this Info.plist into your binary’s __TEXT/__info_plist section. For Swift Package Manager:

// Package.swift
.executableTarget(
    name: "yourcli",
    dependencies: [/* ... */],
    linkerSettings: [
        .unsafeFlags([
            "-Xlinker", "-sectcreate",
            "-Xlinker", "__TEXT",
            "-Xlinker", "__info_plist",
            "-Xlinker", "Sources/Resources/Info.plist"
        ])
    ]
)

This creates a special section in your binary that macOS reads to identify your app. You can verify it worked:

otool -s __TEXT __info_plist yourcli | xxd -r -p | plutil -p -

Step 3: Entitlements for the Paranoid OS

Create an entitlements file - yes, CLI tools can have these too:

<?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>com.apple.security.automation.apple-events</key>
    <true/>
</dict>
</plist>

Step 4: Code Signing (The Final Boss)

Here’s my battle-tested signing script that handles both development and production:

#!/bin/bash
set -e

BINARY_PATH=".build/release/yourcli"

# Check for Developer ID certificate
if security find-identity -p codesigning -v | grep -q "Developer ID Application"; then
    # Production signing
    SIGNING_IDENTITY=$(security find-identity -p codesigning -v | \
        grep "Developer ID Application" | head -1 | awk '{print $2}')
    
    codesign --force \
        --sign "$SIGNING_IDENTITY" \
        --options runtime \
        --entitlements "yourcli.entitlements" \
        --identifier "com.yourcompany.yourcli" \
        --timestamp \
        "$BINARY_PATH"
else
    # Ad-hoc signing for development
    codesign --force \
        --sign - \
        --entitlements "yourcli.entitlements" \
        --identifier "com.yourcompany.yourcli" \
        "$BINARY_PATH"
fi

The Advanced Stuff: Escaping Cursor’s Shadow

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: responsibility_spawnattrs_setdisclaim. I discovered this gem from Qt’s excellent blog post about the responsible process problem.

// Bridge the private API
@_silgen_name("responsibility_spawnattrs_setdisclaim")
func responsibility_spawnattrs_setdisclaim(
    _ attr: UnsafeMutablePointer<posix_spawnattr_t?>,
    _ disclaim: Int32
) -> Int32

// Use it to launch a subprocess that owns its permissions
func launchWithOwnPermissions(path: String, arguments: [String]) throws {
    var attr: posix_spawnattr_t?
    posix_spawnattr_init(&attr)
    defer { posix_spawnattr_destroy(&attr) }
    
    // The magic happens here
    responsibility_spawnattrs_setdisclaim(&attr, 1)
    
    var pid: pid_t = 0
    let argv = ([path] + arguments).map { strdup($0) }
    defer { argv.forEach { free($0) } }
    
    let status = posix_spawn(&pid, path, nil, &attr, argv + [nil], environ)
    guard status == 0 else {
        throw POSIXError(POSIXError.Code(rawValue: status) ?? .ENODEV)
    }
}

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…”

Testing Your Implementation

Reset permissions to test the flow:

tccutil reset AppleEvents com.yourcompany.yourcli

Check your work:

# Verify Info.plist embedding
otool -s __TEXT __info_plist yourcli

# Check entitlements
codesign -d --entitlements - yourcli

# Verify signature
codesign -dv yourcli

The Bottom Line

Making AppleScript work in CLI tools requires:

  1. An embedded Info.plist for identity
  2. Proper entitlements for permissions
  3. Correct code signing
  4. Optional: responsibility_spawnattrs_setdisclaim for cleaner permission dialogs

Building native is hard mode. Every day you learn a new insanity. May your permission dialogs always show the right app name.