programa
programa is a native macOS terminal built for running many coding agents in parallel. Every terminal surface it creates is scriptable through a programa CLI that talks to a local Unix socket — split panes, read a sibling pane's output, send it keystrokes, and get notified, all without the terminal UI itself.
Guard: confirm you're actually inside programa
Check this before anything else in this skill:
if [ -z "$PROGRAMA_SURFACE_ID" ] || [ -z "$PROGRAMA_SOCKET_PATH" ]; then
echo "Not running inside programa (PROGRAMA_SURFACE_ID/PROGRAMA_SOCKET_PATH unset) — skipping programa CLI use."
fi
If either variable is unset, stop here. Don't guess a socket path, don't fall back to a default location, don't try anyway — just say you're not running inside programa and continue with normal shell commands.
Both variables are exported automatically by programa on every terminal surface it creates (no shell integration or setup required), along with PROGRAMA_WORKSPACE_ID. Every command below defaults its --workspace/--surface flags to those env vars when you omit them, so most calls need no flags at all when you're operating on your own pane.
The programa CLI is already on PATH inside a programa terminal. Verify with command -v programa.
Inspecting your surroundings
Run programa tree first — it prints the whole hierarchy (windows → workspaces → panes → surfaces) with markers for where you and the user actually are:
$ programa tree
window window:1 [current] ◀ active
└── workspace workspace:2 "api-server" [selected] ◀ active
├── pane pane:1 [focused] ◀ active
│ └── surface surface:3 [terminal] "zsh" [selected] ◀ active ◀ here
└── pane pane:4
└── surface surface:5 [terminal] "npm run dev"
◀ active— the true focused window/workspace/pane/surface path (where the user's cursor is right now)◀ here— the surface thisprograma treecall was invoked from (you)[selected]/[focused]— that level's current UI selection (not necessarily "active" — the user may be in a different window)
Useful flags:
programa tree --all # every window, not just the current one
programa tree --workspace workspace:2 # scope to one workspace
programa --json tree # structured JSON (global --json flag goes before the subcommand)
Narrower listings, when you don't need the whole tree:
programa list-workspaces # workspaces in the current window
programa list-panes # panes in the current workspace
programa list-pane-surfaces # surfaces (tabs) in the focused pane; add --pane <id> for another
programa identify # your own window/workspace/surface IDs as JSON
All of the above default to your own window/workspace via the env vars when you don't pass --workspace/--window.
Splitting panes and running commands without stealing focus
programa's commands split cleanly into two groups:
- Focus-preserving — safe to call at any time from any agent:
new-split,new-pane,new-surface,send,send-key,send-panel,send-key-panel,read-screen(aliascapture-pane). None of these move the user's cursor, raise the window, or change the active tab. - Focus-changing — only call these when you actually mean to move the user's attention:
focus-pane,focus-window,focus-panel,select-workspace,next-window/previous-window/last-window.
Create a split without touching focus:
programa new-split right # split the current pane
programa new-split down --workspace workspace:2 # split in a different workspace
Text output is OK surface:6 workspace:2 — the new surface's handle. Send it a command without focusing it:
result=$(programa new-split right)
handle=$(echo "$result" | awk '{print $2}') # surface:6
programa send --surface "$handle" "npm run dev\n"
\n (or \r) sends Enter, \t sends Tab, inside the text argument to send/send-panel. Use send-key when you need a literal key event instead of typed text (ctrl+c, enter, arrow keys):
programa send-key --surface "$handle" ctrl+c
new-pane / new-surface work the same way when you want a brand-new pane or an extra tab rather than splitting the current one:
programa new-pane --direction down --workspace workspace:2
programa new-surface --pane pane:4 # new tab in an existing pane
Reading output from a sibling pane
read-screen (alias capture-pane, for tmux muscle memory) returns terminal text as plain text — the visible viewport by default, or scrollback on request:
programa read-screen --surface "$handle" # visible viewport
programa read-screen --surface "$handle" --scrollback --lines 200 # last 200 lines of scrollback
Use this to check a build log, a test runner, or another agent's output without switching to its pane. Treat a single read as a snapshot, not a completion signal — poll it (see "Waiting" below) if you need to know when something finishes.
Spawning a helper agent and coordinating with it
When the agent_spawn tool is available, use it for helper agents. It opens the helper as a nested workspace under the current workspace, keeps the same folder, and does not move the user's focus. Set needs_isolation only when the helper will make conflicting Git changes and genuinely needs a separate worktree. Programa then shows the helper in Agent Overview automatically.
Use a split for a long-running shell command, or as a fallback when agent_spawn is unavailable. Launch the command with send, then treat it like any other sibling pane: read its output, send follow-up input, and report through the sidebar instead of the pane the user isn't looking at.
# 1. Split and capture the new surface's handle
result=$(programa new-split right)
handle=$(echo "$result" | awk '{print $2}')
# 2. Launch the helper agent in it
programa send --surface "$handle" "claude 'fix the failing test in foo_test.go'\n"
# 3. Check on it later without focusing it
programa read-screen --surface "$handle" --scrollback --lines 100
# 4. Answer it if it's waiting on you
programa send --surface "$handle" "yes\n"
Surface status through the sidebar and native notifications rather than only printing to your own pane:
programa set-status build "compiling" --icon hammer --color "#ff9500"
programa notify --title "Helper agent done" --body "Tests pass, ready for review" --surface "$handle"
set-status writes a pill into the sidebar tab row — use a unique key per tool (build, claude_code, ...) so entries don't collide. notify fires a native notification and lights up programa's unread ring/tab indicator for that surface.
Waiting on a server, a test run, or another agent
wait-surface blocks server-side until a surface's output matches a regex or its process exits, so you don't have to poll:
# Block until the build in a sibling pane finishes, up to 2 minutes
programa wait-surface --surface "$handle" --pattern 'BUILD (SUCCEEDED|FAILED)' --timeout 120
# Block until the process in a surface exits
programa wait-surface --surface "$handle" --exit --timeout 600
Exactly one of --pattern <regex> or --exit is required. Match on whatever the process actually prints ("PASS", "Server started", a prompt returning), not a fixed sleep duration. The wait is answered by the app the moment the condition is met — there is no missed-event window even if the output appears while the call is being issued.
wait-surface also has a third condition, --agent-state <idle|working|blocked|any_change>, for a sibling pane running another agent whose lifecycle hooks report status automatically (Claude Code/Codex/OpenCode installs wire this up for you, no extra setup) — block on what the agent is doing, not what it prints:
# Block until the agent in the helper pane goes idle (or has no state at all -- see below)
programa wait-surface --surface "$handle" --agent-state idle --timeout 300
# Block until it flags something it needs you for
programa wait-surface --surface "$handle" --agent-state blocked --timeout 300
A surface that has never reported any state counts as idle for --agent-state idle (most panes have no agent hooks installed, and "idle" almost always means "not currently busy" — which is true of a bare terminal too). blocked/working require an actual report; there's nothing to observe otherwise.
For two cooperating processes, wait-for (tmux-compatible) gives you a named rendezvous instead of scraping a log — one side signals, the other blocks until it does:
# In the helper agent's pane, once it's done:
programa wait-for -S build-complete
# In the coordinating agent:
programa wait-for build-complete --timeout 120
This is a filesystem-based signal, not a verdict on why the other side signaled — pair it with a read-screen check if you need to confirm success vs. failure.
Prompting a helper agent and waiting for it, in one call
prompt-agent combines "send a prompt" and "wait for it to finish" into a single request — for the common case of the coordinating loop in "Spawning a helper agent" above:
programa prompt-agent --surface "$handle" --timeout 300 "fix the failing test in foo_test.go"
It sends the text, waits (briefly) for the helper to report it started working, then waits for it to go idle again. If the helper never reports any activity at all, the JSON response carries a warning noting its hooks may not be installed, rather than hanging or failing outright — check that field if prompt-agent returns suspiciously fast.
Writing a recap
When the user asks for a recap or summary of a change, write it as markdown to .programa/recaps/<slug>.md (repo root resolved with git rev-parse --show-toplevel from your cwd), then open it:
programa recap open <slug>
Use the same panel that renders programa markdown open for it, so lean on its formatting:
```mermaidfenced blocks for flow diagrams (rendered offline, no network call)GitHub-style alerts (
> [!NOTE],> [!TIP],> [!IMPORTANT],> [!WARNING],> [!CAUTION]) for calloutsa
:::compareblock for before/after code, rendered side by side::::compare ```swift before old code ``` ```swift after new code ``` :::
programa recap list shows the slugs already saved. Keep the recap itself short and plain, the same way you'd summarize the change in chat.
Browser work
Two browsers, two jobs. Do not reach for a Chrome extension for either.
Local previews, smoke tests, screenshots you read back, DOM checks, console errors: use programa's embedded browser. It opens beside your pane, keeps its own profile, and never moves the user's focus:
programa browser open-split http://localhost:3000 # prints the new surface id programa browser --surface surface:7 snapshot --interactive # interactive elements only programa browser --surface surface:7 click "button.submit" --snapshot-after programa browser --surface surface:7 screenshot --out /tmp/after.png programa browser --surface surface:7 tab closeprograma browser --helplists the rest (wait, fill, eval, cookies, console, errors). Network routing, viewport, and raw input injection are not supported on WKWebView and returnnot_supported. Over MCP the same calls are thebrowser_*tools ofprograma-mcp.Logged-in sites, private dashboards, CI logs, anything that needs the user's real browser profile: use Aside through its MCP server if it is registered (tools from the
asideserver, oraside-devtoolsfor raw Chrome DevTools control), or delegate a whole task from the shell:aside "Open the staging dashboard and tell me whether the last deploy is green"programa aside statussays whether Aside is installed and registered;programa aside install-mcpregisters it with Claude Code and Codex. Do not run the installer yourself unless the user asks, it edits their agent config.
Reference
--workspace/--surface/--pane/--windowaccept either a short ref (workspace:2,surface:4) or a raw UUID; omitted, they default to$PROGRAMA_WORKSPACE_ID/$PROGRAMA_SURFACE_ID.--jsonand--id-format <refs|uuids|both>are global flags and go before the subcommand:programa --json tree,programa --id-format both list-panes.- Full command list:
programa help. - Anything not wrapped by a dedicated subcommand is reachable directly:
programa rpc <method> [json-params]calls any socket API method. watch-eventsstreams a live feed of agent-state/output/workspace events over one long-lived connection — it's for dashboards and orchestrators watching many surfaces at once, not for a normal agent loop; usewait-surface/prompt-agentfor "wait for one thing" instead.- Longer walkthrough and the full socket API reference:
docs/agent-skill.mdanddocs/v2-api-migration.mdin the programa repo.