The measurement identified realized surfaces by counting shell processes, which is what was available while probing. morning-driver, building against this document, found a cheaper and stronger test in the sdef: `working directory` is readable on a terminal, and a surface with no shell process reports it EMPTY - exactly the ghost signature the broken creation API produces. That turns "has this surface been realized" from an inference into a machine-checkable question, which is the guard anything driving Ghostty needs before it types into a pane. The same property answers one of the document's own open questions: it goes empty when a shell exits and returns when the prompt is back, so "has this pane returned to a prompt" can be polled rather than guessed at with a delay. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011eKRzhD1RqY2MxvDEkrJAM
8.3 KiB
Driving a day plan into Ghostty: what is actually possible on 1.3.1
Measured 2026-08-01/02 against the installed terminal, not reasoned about. This document exists to answer one gate question before any design: can Ghostty be scripted to open a tab with two vertical panes, each in its own directory, with a command running in one?
Short answer: yes, but not through the documented API - the documented path is broken in the installed version, and the path that works is an undocumented composition of primitives that the maintainer has labelled preview.
The environment being measured
- Ghostty 1.3.1 (stable, released 2026-03-13). This is the newest release; there is no 1.3.2 and no 1.4.
- Config:
working-directory = inherit,window-save-state = always,shell-integration = zsh,macos-titlebar-style = tabs. - Operator's workspace: one window, 13 tabs, 2 vertical panes per tab.
What was measured
| Mechanism | Realizes a surface? | Splits? | Can start a command? |
|---|---|---|---|
AppleScript new window / new tab / split |
No | structure only, panes blank | no |
ghostty +new-window |
not available on macOS | - | - |
open -na Ghostty.app --args ... |
yes (26 shells) | yes, whole layout | no, and it duplicates everything |
perform action on a realized surface |
Yes | yes | yes |
1. The documented API is broken
new window, new tab and split return objects with the correct structure -
count of tabs and count of terminals are exactly what was asked for - but no
shell process is ever spawned. Nine probe windows were created across the
session; the count of login ... exec -l /bin/zsh processes stayed at 29
throughout. The windows are visible and blank, and the tabs carry the
placeholder ghost emoji as their name.
Any subsequent call that needs the process fails:
execution error: Ghostty got an error: Terminal surface model is not
available. (-10000)
This is issue #12730,
"AppleScript: new tab via 'new tab' creates empty tab without shell process
(1.3.1 regression)", closed as not planned with no maintainer comment. The
independent measurement here matched the report on every detail including the
ghost emoji and the full set of failed workarounds (delays up to 4s, explicit
focus, explicit activate, every surface-configuration variant).
The likely cause is stated in the issue and is consistent with the 1.3.1 release notes: the fix for issue #11457 (AppleScript implicitly activating the app) appears to have removed the step that also initialized the shell.
Consequence: surface configuration is unusable. Its four useful properties
initial working directory,command,initial input,environment variables- all ride the broken creation path. None of them had any measurable effect: no process, no file written, no directory set.
A ghost surface can be detected in one property read, and this is the cheapest
guard against the whole failure class. working directory is readable on a
terminal, and a surface with no shell process reports it EMPTY - that is exactly
the ghost signature. So "has this surface been realized" is a machine-checkable
question, not something to infer from counting login ... exec -l /bin/zsh
processes as this measurement did. Anything driving Ghostty should wait for a
new surface to report a working directory before typing into it, and refuse to
type into one that never does.
Contributed by morning-driver (2026-08-02) from the sdef plus its own
measurement, after building against this document. The same property answers a
question left open below: it goes empty when a shell process exits and returns
when the prompt is back, so "has this pane finished and returned to a prompt"
can be polled rather than guessed at with a delay.
2. open -na works and is still the wrong tool
open -na Ghostty.app --args --working-directory=... produced 26 live shells in
the correct 13-tab / 2-pane layout. It does this by launching a second
Ghostty instance (pid 80818 alongside the operator's pid 519) which then
restores the saved session, because window-save-state = always.
That makes it useless for a day plan on two counts: it duplicates the entire existing workspace rather than opening a chosen subset, and session restore restores layout and working directories but never running programs - the panes came up as bare shells with no Claude Code. It also leaves the operator with two instances of their whole environment.
3. The path that works: actions on realized surfaces
The AppleScript creation commands are broken. The AppleScript action
mechanism is not, and new_tab / new_split are both in
ghostty +list-actions. Performing them on a surface that is already
realized produces surfaces that are themselves realized:
perform action "new_tab" on <realized terminal> -> +1 live shell
perform action "new_split:right" on <realized term> -> +1 live shell
Measured: 29 -> 31 shells, tab count 13 -> 14, and the operator confirmed a live prompt in the new tab.
input text + send key "enter" then works on those panes. Two panes were
driven to cd into different repositories and report pwd plus their own PID;
both wrote distinct directories and distinct PIDs. This is the full chain, and
it needs no surface configuration at all - the working directory becomes a
typed cd, and the command becomes typed text.
Three constraints any implementation inherits
These came out of the measurement and are not negotiable design details.
- Never target a pane by position.
last tab of wis not the tab that was just created; new tabs are inserted relative to the current tab, not appended. A probe that trusted position typed into a tab it did not create. The new tab must be captured by diffing tabids before and after. This is the failure mode with the worst blast radius: the text lands in whatever program that pane is running, which may be another repo's live Claude Code session. - A new tab inherits the anchor pane's working directory
(
working-directory = inherit), so every pane must becd-ed explicitly. Inheriting silently produces a plan that looks right and points at the wrong repo. count of terminalsis not a reliable pane count. Probe tabs reported 3 and 4 terminal objects where 2 shells existed. Panes must be addressed by the ids captured at creation, never enumerated after the fact.
Recommendation
Both paths, split across two homes - and the split is the recommendation, not a hedge.
- The plan itself - which repos, in what order, with which startup command -
belongs here, as a third rendering of the scan
board.shalready performs, built with exactly the argument--briefwas: it is a lookup over data the scan already holds, it costs zero model calls, androute.shalready derives the per-repo command. Printing that plan as pasteable commands is path (a), and it is complete on its own. - Driving Ghostty belongs in a separate repo that consumes the plan. The
measurement is the argument: this is a version-pinned, undocumented
composition on top of an API the maintainer calls a preview and expects to
break in 1.4, whose documented path is already broken and whose regression was
closed as not planned. That is a maintenance surface with its own release
cadence, its own failure modes, and a blast radius that reaches into other
repos' live sessions. None of that is mailbox transport, and none of it should
be able to break
coord-inboxorboard. Thatboard.shlives here is not an argument for Ghostty automation living here.
The dependency runs one way: the driver consumes the plan, the plan never knows a terminal exists. If 1.4 breaks the driver, the day plan still prints and the operator still pastes.
Not measured
- Whether
input textis safe against a pane running a full-screen TUI. - Whether 1.3.0 is unaffected. The issue calls this a 1.3.1 regression, which implies it, but downgrading a daily driver to gain a preview API that changes in 1.4 was not evaluated as a serious option.
- How "focus" should be expressed, how many tabs a day plan should propose, and whether it should remember yesterday. These were deferred until the gate question was answered; they are now open.