Opening a Door in Someone Else's Terminal: Four Downgrades of VibeTrail's Resume
AppleScript crashes, TCC silent denials, a dead-click investigation — the permission reefs of macOS automation, and the path that was in plain sight all along.
VibeTrail’s core promise is “browse → search → resume”: find that Claude Code session from three weeks ago, click once, and a new window opens in your terminal of choice, cd’s into the project directory, and runs the resume command. Terminal.app and iTerm2 have mature AppleScript interfaces — smooth sailing. Ghostty made me rewrite it four times — each time running aground on a different reef of macOS automation, while the final answer had been sitting in Finder’s right-click menu all along.
Version one: open -n, paid for in Dock icons
Ghostty’s command-line arguments only take effect at process launch, so the most direct way to “open a new window with a directory and a command” is open -n to force a new instance. Functionally correct; the side effect is one extra Ghostty icon in the Dock per resume. For a tool that resumes a dozen times a day, that’s not a blemish — it’s a garbage factory.
Version two: the official scripting API, which crashed the host
Ghostty 1.3 shipped an AppleScript dictionary that can make a running instance open a new window — exactly what I needed. Switched over; clean and tidy. Until, after resuming a few sessions in a row, Ghostty itself crashed.
What I found closed this road for good: the dictionary is officially marked preview, breaking changes are already planned for 1.4, and there are known crash issues on the window/tab creation path. Building your core feature on someone else’s preview interface welds two products’ stability together — and hands them the remote control. Rolled back: cold start goes through open -a (single instance, no new Dock icon); when Ghostty is already running, degrade to “resume command onto the clipboard, prompt the user to paste.”
Version three: the dead-click investigation — TCC’s silent denial
The degraded build went out, and back came the most stubborn symptom there is: click Resume, nothing happens. No error, no dialog, no logs.
First, fix the “can’t see anything” problem: hook window.onerror and unhandledrejection on the frontend with toast reporting — with a “dead click,” half the time nothing ran, half the time something ran and blew up with the exception swallowed. One embarrassing detour worth confessing: I first tried to reproduce the dead click with synthetic pointer events, and the probe itself wasn’t calibrated — it nearly steered me completely wrong. A throwaway click probe confirmed the event chain was intact; the problem was deeper.
The real culprit was in the permission layer. The degraded path’s “write clipboard + activate Ghostty” went through osascript — that is, AppleEvents — which falls under macOS’s TCC (Automation permission). TCC grants are bound to the app’s signing identity, and an unsigned dev build gets a new cdhash on every recompile, invalidating the grant. Once invalidated, the system’s response to the call is a silent denial: no permission prompt, no error return, just pretend nothing happened. That’s the entire mechanism behind “nothing happens.”
The rule this version established matters more than the fix: a fallback path must never depend on a permission that can fail silently. A fallback exists to catch you; a fallback that itself fails without a sound is worse than none — it lets you believe you’re insured. The fix zeroed out the permission surface: clipboard via pbcopy (a process pipe, no permissions), activation via open -a (LaunchServices, no permissions). Terminal and iTerm2 keep AppleScript — they need to execute commands, and AppleEvents can’t be bypassed for that — but that’s the primary path, where failure reports an error. It’s not a fallback.
Version four: the answer was in Finder’s right-click menu
The breakthrough came from switching perspectives: Ghostty gives Finder a “New Ghostty Tab Here” context menu item — how? Read its Info.plist — NSServices. One of macOS’s oldest inter-process integration mechanisms: an app declares a service, the system routes the call, Finder uses it every day, and the vendor maintains it long-term for themselves.
VibeTrail’s final approach: build a filenames pasteboard carrying the target path, then NSPerformService("New Ghostty Tab Here", pboard), invoked through a thin JXA bridge. Pure AppKit, zero AppleEvents, zero permission requests, and behavior byte-for-byte identical to the user right-clicking in Finder — the vendor has already vouched for this path’s stability. Four versions of thrashing, landing on an interface that existed on day one.
Distilled: the forbidden list in the ADR
This history eventually solidified into three prohibitions in an architecture decision record (ADR-4), each one paid for with a corpse:
- No
open -n— multiple instances, multiple Dock icons; - No System Events synthetic keystrokes — requires Accessibility permission (another TCC category that fails silently), and keystroke injection works at the keycode layer: with a Chinese input method active, the injected command comes out as garbage;
- No preview-status scripting dictionaries — crashing the host is unacceptable, and preview means exactly “we will change this whenever we want.”
The same problem’s answers for other terminals, filed for the record: Warp exposes no scriptable “execute a command” surface at all, so it’s the warp://action/new_window?path= URL scheme to open at the directory + command onto the clipboard, honestly telling the user “paste and hit enter.” Cursor has GUI-client semantics, and officially there isn’t even a deeplink for “jump to a specific past session” (still a feature request on the forums) — so ResumeSpec grew a LaunchMode::GuiApp that just does open -a Cursor <project path>, with the session title shown in the UI to help the human locate it.
Two general conclusions:
- When picking a route for macOS automation, prefer the roads the vendor has already paved for system integration — NSServices, URL schemes, document type associations. They serve the vendor’s own user experience, so their stability commitment is inherently higher than a scripting interface that was “added while we were at it”;
- The honest takeaway after adapting three terminals and one IDE: there is no unified abstraction for the last mile of resume. Every product’s capability surface, permission surface, and stability commitments differ. The only reusable thing is the methodology — first map out which interfaces the other side maintains and for whom, then decide which one to stand on.