Control API

Command reference

All 74 commands of the agterm control API, with arguments and return values. Every one is reachable from the agtermctl CLI and from the local control socket.

For the narrative introduction — what the socket is, how to install the CLI, and worked recipes — see the agtermctl section of the docs.

Overview

agterm listens on a local unix-domain socket. Each connection carries one newline-delimited JSON request and receives one response, then closes. The scope is personal scripting: fire-and-forget commands, plus a polled event feed (events.read) for watching status and lifecycle changes. There is no scrollback or terminal-output streaming.

Socket resolution. With --socket omitted, agtermctl resolves the same rendezvous the app bound: <AGTERM_STATE_DIR>/agterm.sock, else ~/Library/Application Support/agterm/agterm.sock. A spawned shell also sees the bound path in $AGTERM_SOCKET; passing --socket "$AGTERM_SOCKET" is the safe explicit form.

Response shape. {"ok": true, "result": {…}} or {"ok": false, "error": "<message>"}. The process exit code is non-zero when ok is false. result carries one of: id (affected/new session, workspace, or window), text (session copy/text), exitCode (overlay result), count (search matches, keymap/config diagnostics), ratio (session resize), tree, or windows.

Options go after the subcommand. agtermctl session type "ls" --target active, never before it. Add --json to print the raw response object — without it, mutations print ok and tree / window list print a human listing. Use --json when you need to read ids or values back.

Addressing

--target defaults to active (the selected session, or the current workspace). It accepts a full UUID (case-insensitive) or a unique, git-style prefix. Zero matches gives notFound; an ambiguous prefix gives ambiguous, listing the candidates. For a workspace, active is where a new session lands: one you just created in the foreground, or an empty one you named with workspace select, until the selection changes, workspace select names a workspace that has sessions, that workspace is deleted, or the workspace filter hides it; otherwise the selected session's workspace, otherwise the last one. A background create (workspace new --collapsed, session new --create-workspace --no-select) never takes it, and the tree's workspace active flag reads the selected session's workspace only, so right after a foreground create the two can name different workspaces.

--window <id|prefix|active> (on session, workspace, tree, font, notify, and pick commands) picks which window's tree to act on; the default is the frontmost. With it set, that window must be open; without it, an id/prefix session target is matched across all open windows. The window.* commands instead take the window selector as a positional argument, defaulting to active (frontmost). A window need not be open to be a window.* target — window select opens a closed one.

For an agent, active is the user's GUI-selected session, not yours. Your own shell is $AGTERM_SESSION_ID. Pass --target "$AGTERM_SESSION_ID" on any session-scoped command that must act on the session you run in — otherwise it hits whatever the user has selected.

tree

agtermctl tree [--json] [--window W]
tree

Print the workspace/session tree. This is the read side of most of the API — nearly every state-mutating command has a matching field here, so a script can record a value, change it, and restore it.

Session node: id, name, cwd, title (raw OSC terminal title, omitted when none), active, split, splitRatio, splitFocused, overlay, overlaySizePercent, paneOverlays (the panes covered by their own pane-scoped overlay — ["left"], ["right"] or ["left","right"], omitted when neither is), hud (the message panel holding the session-wide slot — {message, detail?, spinner, backgroundColor?, textColor?, sizePercent?, heightPercent?, position}, where spinner names the effective style or none — omitted when none is up; mutually exclusive with overlay, which reads false beside it), scratch, flagged, status, statusPane, statusBlink, statusColor, statusShape, foreground / splitForeground (each pane's live foreground argv, omitted at the shell prompt or for a setuid program like top/sudo), background, restoreCommand / splitRestoreCommand (each pane's pinned restore override, omitted when none and an empty string when pinned to nothing), commandWait (a held --command session created with session new --wait; omitted otherwise), and unseen.

Workspace node: id, name, active, sessions, focused (membership in the sidebar focus set — the read side of workspace focus, distinct from active; reported independently of whether the filter is applied, so a workspace row renders iff sidebarVisible && sidebarMode == "tree" && (!workspaceFilter || focused) — no workspace row at all with the sidebar hidden or in flagged mode, the whole tree while the filter is off, only the members while it is on), and collapsed (the read side of workspace collapse/expand; true when collapsed, omitted when expanded).

Top level: idleMs (ms since last user input; live, so tree-only), autoFollowMs, sidebarVisible, sidebarMode, workspaceFilter (whether the workspace focus filter is applied — the flag half of the focus set, the read side of workspace filter), quickVisible, and pickPending (the pending native picker's id, omitted when none is open). All seven are read-only projections of GUI state.

events

agtermctl events [--json] [--kind KIND ...] [--run UUID --after SEQ] [--limit N]
events.read

Read the app's control-event ring. The CLI polls it in a loop and prints one event per line, so a script can watch agent status, notifications, and session lifecycle instead of re-reading tree. With no cursor the first read subscribes from the current tail: it returns an empty batch anchored at run/next and replays no history. The ring keeps the latest 4,096 events of one app process and is non-destructive, so independent readers never consume one another's events.

Kinds. status, notify, session.created, session.closed, and tree.changed (a 100 ms coalesced signal that a window's names, membership, or ordering moved — read tree --json for the new snapshot). --kind may be repeated or comma-separated; an unknown kind errors. Every event carries seq, ts, kind, the applicable window/workspace/session ids, and a kind-specific payload — a status payload carries the session name, the normalized status, blink, and the optional pane, color, and shape fields.

Cursors. The raw response returns the batch under result.events as {run, next, items}. Resume with both values — --run RUN --after NEXT, which must appear together. --limit defaults to 100 and accepts 1 through 1,000, and a filtered read still advances the global cursor past nonmatching events. The streaming --json output is bare event objects, not the batch envelope, so a restart-safe client keeps the cursor from raw events.read responses.

A changed app run, an expired cursor, or a cursor ahead of the current sequence is a hard error carrying the ring's current anchor — treat it as a data-loss boundary rather than silently rebaselining. agtermctl events exits non-zero on those, on a server error, and on a missing app or socket. There is no terminal-output or scrollback stream.

workspace

agtermctl workspace new [name] [--collapsed] [--window W]
workspace.new

Create a workspace. The name defaults to an auto-generated one. --collapsed creates it closed in the sidebar tree, so a script can fill it with session new --no-select without it opening — and for the same reason a collapsed create stays OUT of the workspace focus set. A plain create instead joins the marked set while the filter is applied, so a foreground workspace is never hidden behind it. Returns result.id.

agtermctl workspace rename <name> [--target T] [--window W]
workspace.rename

Rename the target workspace.

agtermctl workspace delete [--target T] [--window W]
workspace.delete

Delete the target workspace. Keep-at-least-one: deleting the last workspace is an error. Unlike the GUI, nothing blocks on a confirm dialog.

agtermctl workspace select [--target T] [--window W]
workspace.select

Select the target workspace.

agtermctl workspace move --to up|down|top|bottom [--target T] [--window W]
workspace.move

Reorder the workspace among its siblings. A missing or invalid --to is an error.

--target active resolves to the current workspace — one you just created in the foreground, otherwise the selected session's, otherwise the last one — so address a specific workspace by id to step the same one repeatedly.

agtermctl workspace focus [on|off|toggle|add] [--target T] [--window W]
workspace.focus

Mark or unmark one workspace in the sidebar's focus set. The sidebar renders the marked workspaces while the filter is applied, all of them while it is not. on sets the marked set to just this workspace and applies the filter, off removes it (the filter switches off once the set empties), toggle (the default) replace-toggles, and add inserts it alongside the others while leaving the filter flag exactly as it was. Per-window and persisted; orthogonal to sidebar mode. An unknown mode errors. Returns result.id.

add never switches the filter on — that is what makes a multi-workspace set buildable, since a mark that narrowed the tree would hide the rows still to be marked. Mark several, then apply once with workspace filter on. While the filter is applied, session go navigation is scoped to the marked workspaces' sessions, and an explicit session select of a session outside the set suspends the filter while keeping the set — in TREE mode only, since the flagged list ignores the marked set, so a selection made there leaves the filter applied. Read membership back via the workspace node's focused field.

A narrowing mode CAN move the selection: when on, a narrowing toggle, or an off that drops the selected session's workspace while other members keep the filter applied would hide the selected session, the most recently used session still visible is selected instead. Read it back as active on the session node. add normally narrows nothing and leaves the selection alone — except while the marked set holds only session-less workspaces, where nothing was visible to move to: adding a populated workspace then selects its most recent session.

agtermctl workspace filter [on|off|toggle] [--window W]
workspace.filter

Apply or suspend the whole window's workspace focus filter without touching the marked set, so peeking at the full tree and coming back costs one call each way. Window-scoped: it takes no --target, and --window picks the window (default frontmost). toggle is the default; idempotent. An unknown mode errors, and no open window when none is open. Per-window and persisted, like the marked set itself, so a relaunch restores whether the filter was applied.

on with an empty marked set is refused — it returns ok having changed nothing, so an applied filter always has at least one visible member, which is what keeps the filter term of the row-visibility read-back exact (the full predicate, including the sidebar-mode term, is on the focused field in the tree section). Read back via the tree's top-level workspaceFilter field.

Applying the filter CAN move the selection: if it would hide the selected session, the most recently used session still visible is selected instead — read back as active on the session node. The same holds for sidebar mode flagged. A script that narrows and then relies on the default active target should re-read tree first.

agtermctl workspace collapse [--target T] [--window W]
workspace.collapse

Collapse a single workspace's subtree in the sidebar tree, hiding its sessions. The per-workspace counterpart of sidebar collapse (which collapses every workspace but the active one) — this targets exactly the addressed workspace. Idempotent and persisted. Returns result.id.

Read the open/closed state back via the workspace node's collapsed field (true when collapsed, omitted when expanded).

agtermctl workspace expand [--target T] [--window W]
workspace.expand

Expand a single workspace's subtree, showing its sessions — the inverse of workspace collapse and the per-workspace counterpart of sidebar expand. Idempotent and persisted. Returns result.id.

To toggle a workspace, read its collapsed field off tree first, then call expand or collapse.

session

agtermctl session new [--cwd DIR] [--workspace W | --workspace-name NAME [--create-workspace]] [--command CMD] [--wait] [--name NAME] [--after SID | --before SID] [--no-select] [--window W]
session.new

Create a session and focus it. --cwd sets the start directory (default $HOME). The destination workspace is addressed either by --workspace (id / prefix / active) or by --workspace-name (the sidebar label) — the latter errors when no workspace has that name, unless --create-workspace is also passed, which reuses or creates it idempotently. --name seeds the sidebar label. Returns result.id.

--after SID / --before SID place the new session directly after or before an anchor session instead of appending. The anchor carries its own workspace, so it names the destination itself — these are mutually exclusive with each other and with --workspace / --workspace-name. The headline case: session new --after active.

--command runs a program as the session's process instead of the login shell (no echoed command line; the session closes when it exits). It runs argv-style — tokenized with quotes respected, but no shell, so ;, &&, $VAR, redirects and globs are not interpreted. It also inherits the app's GUI PATH (the launchd default — no /opt/homebrew/bin), so a bare Homebrew binary fails with exit 127. Use an absolute path, or wrap it: --command "zsh -lc 'htop'". The command is persisted and re-runs on restore when Restore running commands on restart is enabled.

--no-select creates the session in the background: it is added to the sidebar but not selected or focused, leaving the current selection untouched (the new node is not active in tree — that flag is the read-back). Omit it for the default select-and-focus behavior.

--wait (only with --command, else an error) holds the session open after the command exits — the press-any-key prompt with the final output intact instead of closing — so a build, test, or deploy's last output (or an early failure) stays readable. It persists across restart, so a restored command session that re-runs its command holds again. Read it back on tree's commandWait.

agtermctl session duplicate [--target T] [--window W]
session.duplicate

Create a fresh session in the same workspace as the target, directly after it, rooted at the target's focused-pane working directory — then select and focus it. There are no other options: the target names both the destination workspace and the directory. Equivalent to session new --cwd <source cwd> --after <source> in one round-trip. Returns result.id.

Only the directory carries over. The duplicate is a plain login shell with the auto basename — it does not inherit the source's custom name, --command, split, scratch, status, flag, font size, or background. It is the control half of the sidebar row's Duplicate Session context-menu item (single-selection only). Read back from tree: no new field — the new session node appears directly after its source, carrying the source's focused-pane cwd (equal to the source node's tree.cwd unless the source is a split focused off its primary pane, where tree.cwd reports the primary).

agtermctl session close [--target T ...] [--window W]
session.close

Close the target session. Repeat --target to close a batch as one grouped undo when close grace is enabled, or immediately when it is disabled. Batch output reports the number of sessions actually closed.

agtermctl session select [--target T] [--window W]
session.select

Select the target session. Selecting one outside the marked workspaces suspends the focus filter to reveal it, keeping the marked set — re-applying it costs one workspace filter on.

agtermctl session rename <name> [--target T] [--window W]
session.rename

Set the session's custom sidebar label.

agtermctl session reveal [--target T] [--window W]
session.reveal

Select the target session's focused-pane working directory in Finder. Errors if the directory no longer exists.

agtermctl session move <workspace> | --to up|down|top|bottom | --after SID | --before SID [--target T ...] [--window W]
session.move

Three mutually-exclusive placement intents, exactly one required. A positional <workspace> relocates the session there (appending). --to reorders it within its own workspace. --after / --before place it directly after or before an anchor session — the anchor carries its own workspace, so cross-workspace placement falls out for free. Repeat --target with a workspace or --after / --before to move an ordered batch atomically; batch relative reorder via --to is not supported. Batch output reports the number of sessions actually moved.

agtermctl session go --to next|prev|first|last|next-attention|prev-attention [--window W]
session.go

Move the selection relative to the current one — there is no --target. It operates over the visible, filtered set: the flagged sessions in flagged mode, the marked workspaces' sessions while the focus filter is applied, else all. next/prev wrap at the ends; next-attention / prev-attention step only through sessions needing attention (status blocked or completed), also wrapping. Returns the newly selected result.id.

The attention variants only step the selection; unlike the GUI attention-nav they do not themselves move focus into a tagged pane.

agtermctl session type <text> [--stdin] [--select] [--pane left|right|scratch] [--target T] [--window W]
session.type

Inject text as real keystrokes — printable runs plus a Return for each newline, with no bracketed-paste markers. So a trailing newline submits the command. --stdin reads the text from stdin instead of the argument. Any session is typable without --select, including one created moments ago: the main pane waits briefly for its surface, so creating and typing back to back does not race. --select selects the session first, and only when its surface is not ready yet.

--pane left is the main pane (the default), right the split pane, and scratch the scratch terminal even while hidden. Note the shell quoting: a literal \n inside plain single quotes reaches the CLI as two characters — use $'make test\n' or pipe a real newline via --stdin.

agtermctl session copy [--target T] [--window W]
session.copy

Returns result.text with the session's current selection. It does not touch the system clipboard — pipe the returned text into another session type. Selection is surface state independent of focus, so any realized session can be read. No or empty selection gives a no selection error.

agtermctl session paste [--target T] [--window W]
session.paste

Paste the system clipboard into the session's main pane — the socket analogue of ⌘V / Edit ▸ Paste. It runs libghostty's paste_from_clipboard as a bracketed paste with no prompt, so the text lands at the prompt without auto-submitting.

Read it back with session text. A never-shown session gives session not realized.

agtermctl session select-all [--target T] [--window W]
session.selectall

Select the session's entire terminal buffer (main pane) — the socket analogue of ⌘A / Edit ▸ Select All, running libghostty's select_all.

Read the resulting selection back with session copy. A never-shown session gives session not realized.

agtermctl session text [--all] [--lines N] [--pane left|right|scratch] [--target T] [--window W]
session.text

Returns result.text with the terminal buffer as plain text (no ANSI or color). By default it reads the visible screen of the on-screen pane. --all adds scrollback; --lines N keeps only the last N content lines. The two are mutually exclusive and N must be greater than 0 (enforced server-side too).

A genuinely blank screen is not an error — it returns ok with an empty string, unlike session copy's no selection. A failed read is an error (failed to read surface buffer). Plain text only; there is no --ansi.

agtermctl session search [needle] [--next | --prev | --close] [--target T] [--window W]
session.search

Search the session's live scrollback. It selects the target first, so the search bar and match highlights render. With a needle it sets the query (opening the bar if needed); with no needle and no flag it just opens an empty bar. The three flags are mutually exclusive. Returns result.count (total matches) and result.text (the counter string — "N of M", "M matches", or "no matches").

The count settles asynchronously, so the command waits briefly for it.

agtermctl session split [on|off|toggle] [--target T] [--window W]
session.split

A side-by-side second shell in the same session. off hides it but keeps the shell alive (mirroring ⌘D); the pane's surface is torn down only when its own shell exits. Idempotent; an unknown mode errors.

agtermctl session scratch [on|off|toggle] [--command CMD] [--target T] [--window W]
session.scratch

A third, full-coverage shell that renders like a full overlay but behaves like the split. off hides it keep-alive; typing exit closes it and the next on spawns a fresh shell. on selects the target first. Not persisted; an unknown mode errors.

--command (only when showing) runs a program instead of a login shell — argv-style with the same GUI-PATH exit-127 caveat as session new --command, and run-once. A scratch is expendable, so passing --command while one is open respawns it.

agtermctl session focus [left|right|other] [--target T] [--window W]
session.focus

Move keyboard focus between the two split panes; other toggles and is the default. Errors when the session has no split. It works whether the split is shown side-by-side or hidden — when hidden, focusing a pane swaps which one shows maximized. Read back via the session node's splitFocused.

agtermctl session resize (--split-ratio R | --grow-left D | --grow-right D) [--target T] [--window W]
session.resize

Move the split divider. Provide exactly one form: --split-ratio sets the absolute left-pane fraction (0–1); --grow-left D / --grow-right D nudge it by the fraction D. The result is clamped to 0.05–0.95 and persisted. Returns the applied fraction as result.ratio. Errors when the session has no split.

Control-native: the divider is otherwise mouse-only — drag it, or double-click it for an even split. No GUI, menu, or keymap action reaches any other fraction — bind a key by mapping a command "agtermctl session resize …" custom action. Resizing a hidden split updates the stored fraction, applied when it is next shown.

agtermctl session status <idle|active|completed|blocked> [--blink] [--auto-reset] [--sound NAME] [--color #rrggbb] [--shape SHAPE] [--pane left|right|scratch] [--pane-id TOKEN] [--target T] [--window W]
session.status

Set the sidebar agent-status glyph. Setting a non-idle status is for agents and hooks; idle clears it (also available in the GUI). An unknown state errors. --blink requests an attention pulse; macOS Reduce Motion suppresses the repeating sidebar and dashboard animation while keeping the status visible, and the pulse resumes when Reduce Motion is disabled; --auto-reset clears it back to idle once the session is visited (a one-shot completion flash).

--sound plays a one-shot sound: default or a system sound name (Basso, Glass, Ping, … plus any custom sound in ~/Library/Sounds); an unknown name errors. Without it, a blocked status plays the user's configured Blocked sound, if any. --color #rrggbb overrides the glyph tint for this call only; the next status set without it reverts. --shape (circle, square, triangle, diamond, capsule, star) overrides the glyph silhouette the same way, falling back to the Settings shape for that state; an unknown name errors and leaves the status unchanged.

--pane (default left) records which pane set the status. It makes keystroke-clear pane-scoped — a status set from a background pane survives typing in a different pane — and it makes any user-initiated GUI selection of a blocked or completed session reveal and focus the tagged pane. An active status preserves the current pane selection. An agent running in a split or scratch should set its own pane. Read back as the session node's statusPane, statusBlink, statusColor, and statusShape (the last two reporting the per-call override only). --pane-id is the surface's stable spawn token (the shell's $AGTERM_PANE_ID), forwarded automatically by the agent-status hook; when it resolves it overrides --pane, so a status from a promoted-then-re-split pane lands on its current slot. Scripts leave it to the hook.

agtermctl session flag [on|off|toggle|clear] [--target T] [--window W]
session.flag

Flag or unflag a session for the flagged working-set view — a durable, persisted membership. on/off/toggle act on the target and are idempotent; clear ignores the target and unflags every session in the window. Unknown mode errors. Pair with sidebar mode flagged. Read back via the node's flagged field.

off CAN move the selection: unflagging the selected session while the flagged view is up removes the only row pointing at it, so the most recently used session still flagged is selected instead — read back as active on the session node. on moves it the other way when the flagged view is up with nothing flagged: the first flag puts a row on screen and selects it, unless nothing was selected to begin with. clear leaves nothing to move to, so it never does.

agtermctl session seen [--target T] [--window W]
session.seen

Clear the session's unseen-notification badge without changing the selection, focus, or agent status — the focus-free counterpart to notify. Idempotent. Read the current count from the node's unseen field.

Lets an orchestrator acknowledge a driven session's notifications over the socket, keeping the badge a real attention signal on the sessions a human tends.

agtermctl session background image <path> [--opacity F] [--fit contain|cover|stretch|none] [--position P] [--repeat]
agtermctl session background text <text> [--color #rrggbb] [--opacity F] [--fit …] [--position …]
agtermctl session background color <#rrggbb>
agtermctl session background clear
session.background

Set or clear a per-session background composited behind the terminal grid. Persisted, so it survives a relaunch. All four forms accept [--target T] [--window W].

image — PNG or JPEG only; libghostty auto-fits it and re-fits on resize. --opacity is 0.0–1.0 (default 1.0), --fit defaults to contain, --position is center or an edge/corner anchor, and --repeat tiles. text — rasterizes a word or two (capped at 256 characters); --color defaults to the terminal foreground.

color — a solid terminal background color, taking no opacity: it is drawn at the Settings window translucency, so it honors your opacity and blur. macOS Reduce Transparency temporarily presents it as opaque and unblurred without changing the saved opacity or blur; the requested presentation returns when Reduce Transparency is disabled. An image/text watermark instead forces the pane opaque. Read the current spec back from the node's background object.

agtermctl session restore (<command> | --none | --clear) [--pane left|right] [--pane-id TOKEN] [--target T] [--window W]
session.restore

Pin the command a pane re-runs on the next launch, overriding the captured foreground. Provide exactly one form: a <command> shell line to pin, --none to pin nothing (the pane restores a plain shell, suppressing the captured command), or --clear to drop the override and go back to auto-capture. Read back as the session node's restoreCommand (main pane) or splitRestoreCommand (split pane).

The override is written now and consumed on the next launch — it never touches the running session — and it is sticky: it fires again on every restart until cleared. It wins over the session's own session new --command as well as the captured foreground — a pinned line (or --none) suppresses it, so a restored command session no longer takes the exec path or its --wait close-on-exit behavior. It is gated on the Restore running commands on restart setting (a pinned command while the setting is off succeeds with a note that nothing will run; --none/--clear get no note, since their outcome is delivered either way) but bypasses restore-denylist.conf, since it names its command deliberately. A split hidden at quit is not restored, so its pin is dropped on that launch rather than left to fire into a later manual split. It exists for non-idempotent commands like claude --resume … --fork-session: a SessionStart hook can rewrite it to the live session id on every start so the next restart reattaches instead of forking.

--pane (default left) picks the pane; right needs a split, and scratch is rejected (the scratch is never restored). --pane-id (the shell's $AGTERM_PANE_ID) resolves the pane's live slot; unlike session status, a token that does not resolve is an error unless --pane is also given as the fallback. The pinned value is shell code stored in the window's state file and readable via tree, so it must not carry secrets. Not to be confused with restore clear, which is app-global and clears every session's captured command.

session overlay

An ephemeral terminal running one program on top of a session. It closes when the program exits.

agtermctl session overlay open <command> [--cwd DIR] [--wait] [--block] [--size-percent N] [--background-color #rrggbb] [--follow] [--pane left|right] [--target T] [--window W]
session.overlay.open

Full-size by default, hiding the session; --size-percent N (1–100) makes it a floating framed panel with the session visible behind. Returns the overlay's result.id. --background-color gives the overlay pane its own solid color, independent of the session's.

It does not switch the active session by default — both variants open on --target and run in the background. Pass --follow to select the target too. An automated caller should pass --target "$AGTERM_SESSION_ID", or a blocking full-pane overlay lands on whatever session the user has selected.

--wait keeps the overlay open after the command exits (press a key to close). --block waits for the command and makes agtermctl exit with its status — it cannot combine with --wait. The program's output is its own concern; the control channel does not capture stdout.

Unlike session new --command, the overlay command runs through sh -c, so shell operators do work — but it still inherits the app's GUI PATH, so a bare Homebrew binary fails with exit 127 (the overlay flashes open then vanishes). Give an absolute path or wrap in "zsh -lc '…'".

--pane left|right scopes the overlay to ONE split pane, leaving the sibling pane visible and interactive. The two panes are independent and may both hold an overlay at once, each with its own --background-color and --cwd. A pane overlay is always full-pane, so --pane cannot combine with --size-percent; everything else matches the session-wide overlay. A non-split session accepts --pane left (its shell reports AGTERM_PANE=left), so a caller can pass --pane "$AGTERM_PANE" without checking the split state. Errors pane overlay already open on a second open, and pane not visible when that pane is not currently rendered — hiding the split AFTER opening is fine. Read the covered panes back from the session node's paneOverlays; each one also appears in surfaces as overlay-left/overlay-right for surface zoom.

agtermctl session overlay resize (--size-percent N | --full) [--target T] [--window W]
session.overlay.resize

Resize an already-open overlay in place. Exactly one of --size-percent N (1–100, floating) or --full is required; both, neither, or an out-of-range percent errors. The program keeps running across the resize — it is a layout re-flow, never a re-spawn. Errors no overlay when none is open.

Record the node's overlaySizePercent first to restore the exact size after a zoom to --full. It takes no --pane: pane overlays are always full-pane, and passing one errors.

Against a HUD a percent is accepted and re-flows its WIDTH (the height stays measured from the message, which a resize does not change), but --full errors a hud is always floating: pass --size-percent, not --full — full size would cover the session the message is about.

agtermctl session overlay close [--pane left|right] [--target T] [--window W]
session.overlay.close

Close and destroy the overlay. --pane closes that split pane's overlay; omit it for the session-wide one. It takes a HUD down too, as a courtesy — the slot is the same one.

agtermctl session overlay result [--pane left|right] [--target T] [--window W]
session.overlay.result

Returns result.exitCode once the overlay has closed. Errors overlay still running while it is up, and no overlay result if none ran. --pane reads that split pane's overlay; omit it for the session-wide one.

A HUD runs the app's own painter, not a caller's program, so there is no status to report and the session-wide arm errors no overlay result: the slot holds a hud. The --pane arm is unaffected — a HUD only ever takes the session-wide slot.

session hud

A passive message panel in the session's overlay slot. It carries text rather than a program, takes no input, and leaves the session focused, typable, undimmed, and clickable underneath — for the seconds before a caller can show anything. One slot, so a session holds either a HUD or a program overlay, never both.

agtermctl session hud [open] <message> [--detail T] [--spinner] [--spinner-style S] [--position P] [--background-color #rrggbb] [--text-color #rrggbb] [--size-percent N] [--target T] [--window W]
session.hud.open

Post the panel. Returns the session's result.id. open is the group's default subcommand, so session hud "gathering options…" posts one; a message that is literally update or close needs the explicit hud open verb.

--detail adds a dim second line, --spinner animates a glyph in the default bar style while --spinner-style bar|braille|circle|blocks|dot picks another and turns the spinner on by itself (none is accepted too and leaves the panel static, so the value a read-back reports round-trips) (dot blinks rather than animating, for a panel that sits up for minutes), and --position anchors the panel to any of the nine top-left|top-center|top-right|center-left|center|center-right|bottom-left|bottom-center|bottom-right (default center), the same anchors session background takes. Every anchor off center holds a fixed margin off that pane edge on its own, so even the largest allowed panel stays inside the pane, and a corner keeps a long-lived panel clear of the text being read. The bare top and bottom this argument shipped with are still accepted for the middle column, and the read-back reports the canonical anchor. The panel is measured from the message against the session's terminal font on BOTH axes separately — width from the longest wrapped line, height from the number of them — so a title and a subtitle give a wide, short panel rather than a square one. --size-percent N (1–100) overrides the WIDTH only; the height always follows the message, since a caller-set height could only strand it in an empty box. The effective width is bounded to 10–80% of the pane, so a message never covers the session it is about and a requested 100 reads back as 80. --background-color gives the panel its own solid background, read once when the panel is created, while --text-color colors the TEXT and rides the panel's body file, so an update can change it. Both effective shares read back, as sizePercent and heightPercent, and both colors as backgroundColor and textColor.

Message and detail cap at 256 characters and reject control characters, newline included — the panel prints straight into a live terminal, and --detail is the second line on offer. A second hud replaces the first and session overlay open replaces a HUD, but a HUD over a RUNNING program errors overlay already open — a message is replaceable, a program is not.

There is no menu item, chord, or palette entry: this is control-only, with nothing for a human to invoke by hand.

agtermctl session hud update <message> [--detail T] [--spinner] [--spinner-style S] [--position P] [--text-color #rrggbb] [--size-percent N] [--target T] [--window W]
session.hud.update

Repaint the live panel in place — no re-spawn, no blink. It REPLACES the whole spec rather than patching it, so --detail, --spinner, --position, and --text-color must be repeated to survive; an omitted one drops. Same required message and same rejections as open. Errors no hud when none is up.

It takes no --background-color: the surface reads that once at creation, so only a fresh session hud can change it. A color sent on an update over the raw protocol is ignored for the same reason, and the read-back keeps naming the color the panel actually paints. --text-color is the half that CAN change: it rides the body file the panel's painter re-reads every tick, so the live text recolors with no re-spawn.

agtermctl session hud close [--target T] [--window W]
session.hud.close

Take the panel down. Errors no hud when none is up, so it is not idempotent. A program overlay in the same slot is left alone; session overlay close, ⌘W, and closing the session also tear a HUD down.

Read the panel back from the session node's hud object, whose position and spinner always report the effective value, defaults included. Beside it the node's overlay reads false with overlaySizePercent omitted, so a poll for "is a program covering this session" cannot mistake a message for one, and surface zoom will not address the panel. HUD state is poll-only; no event announces it.

window

These take the window selector as a positional argument (default active, the frontmost). A window need not be open to be a target.

agtermctl window new [name] [--minimized]
window.new

Create and open a window. It replies only once the on-screen window exists, so an immediate window resize/move on the returned id works. --minimized parks it in the Dock right after creating it, leaving frontmost on a window you can still see — for building a set of project windows and ending up on one you are looking at. The window is presented briefly before it is parked. Returns result.id.

agtermctl window list
window.list

Returns result.windows, each with id, name, open, active, autoFollowMs, sidebarVisible, geometry ({x, y, width, height, display}, the read side of window move/resize, in the same units they take), plus fullscreen, zoomed and minimized. The last four are omitted for a closed window. A minimized window still reports its geometry — the frame it comes back to.

geometry/fullscreen/zoomed/minimized stay current across hand-drags and GUI toggles. Unlike tree, this does not carry idleMs — a live metric would freeze in the cache.

agtermctl window select <id>
window.select

Raise the window if open, else open it.

agtermctl window close <id>
window.close

Close the on-screen window; the bundle is kept, so window select reopens it.

agtermctl window rename <id> <name>
window.rename

Rename the window.

agtermctl window delete <id>
window.delete

Delete the window bundle entirely. Keep-at-least-one: deleting the last errors.

agtermctl window resize <id> --width W --height H
window.resize

Frame size in points. The window must be open. The size is clamped into the window's minimum and the display's visible frame, so an oversized request is bounded rather than applied verbatim. Control-native (the title bar already drags-to-resize).

agtermctl window move <id> --x X --y Y [--display N]
window.move

Top-left position in points relative to display N (default the window's current display; y measured from the display top). The window must be open. The origin is clamped so an off-screen request keeps a grabbable strip on the target display. Control-native.

agtermctl window zoom <id>
window.zoom

Toggle between the normal frame and a maximized, fill-screen frame — not native full screen. A second call restores the prior frame. The window must be open. This is the control half of the double-click-on-header gesture. Read back via window list's zoomed.

agtermctl window fullscreen <id>
window.fullscreen

Toggle native macOS full screen — a separate Space with an auto-hidden menu bar. A second call exits. The window must be open. The control half of View ▸ Toggle Full Screen (⌃⌘F) and the green traffic-light button; distinct from zoom. Read back via window list's fullscreen.

agtermctl window minimize <id> [on|off|toggle]
window.minimize

Minimize a window to the Dock, or restore it. The mode resolves against the window's current state, so on/off are idempotent and only toggle (the default) flips. Both positionals are optional, so window minimize on targets the active window. The window must be open, and one in native full screen is rejected. The control half of ⌘M, the yellow traffic-light button, and the Minimize title-bar double-click action. Read back via window list's minimized.

Give every window the same frame and park all but one, and switching windows looks like switching a tab. The minimized state is live-only — it is never persisted, so windows reopen un-minimized after a restart.

surface zoom

A view mode that fills the window with one terminal surface. It never touches the macOS window frame — distinct from window zoom.

agtermctl surface zoom [show|hide|toggle] [--target surface:<id>:right | active | quick] [--window W]
surface.zoom

Fill the window with one terminal surface, hiding the sidebar and collapsing the title bar to a slim strip (traffic lights plus an exit button). Omit --target (or pass active) to zoom the active surface — the quick terminal if shown, else the active session's overlay, scratch, the focused pane's own overlay, or that pane. An explicit surface:<session-id>:<left|right|scratch|overlay|overlay-left|overlay-right> id (from tree's surfaces[].id) zooms that exact surface, including a hidden-but-alive split or scratch; quick addresses a quick-terminal zoom.

show is idempotent; hide exits and is idempotent too (an explicit id clears only that target and succeeds even if the surface has since vanished); toggle enters when unzoomed and exits when that surface is already zoomed. Zoom must not mutate split ratios, focus, sidebar state, or split/scratch visibility. Control half of ⌘⇧Return / View ▸ Toggle Terminal Zoom and the title-bar exit button.

Read the current zoom back from the tree's top-level zoomedSurface (the zoomed surface's control id, or quick; omitted when nothing is zoomed).

dashboard

A per-window, view-only grid of live terminal panes. Reciprocally exclusive with surface zoom: opening one closes the other.

agtermctl dashboard <ids[:left|:right]…> [--mru] [--font-size N | --auto-size] [--close] [--window W]
dashboard

Open a grid of the named sessions' live panes, populate it from the window's most-recently-used sessions with --mru, or --close the open one. The cell unit is a session+pane: a non-split session is one cell, and a split session shows as two — its left/primary and right/split panes — so the 9-cell cap counts panes (laid out ceil(√n)). Positional ids are session addresses (id / unique prefix / active), each optionally carrying a :left/:right pane suffix — the same form dashboardMembers reports — which places that pane alone, so dashboard A:left B:right grids one pane per session while a bare id still takes all of them. Cells are deduped by session+pane, so a bare id beside a pane ref for the same session collapses. Any other suffix (:scratch, :primary, a typo) is rejected outright; unresolved ids — including :right on a session with no split — are dropped and any panes beyond 9 are trimmed, both reported in the response text. --mru is mutually exclusive with ids and --close, composes with the font flags and --window, and errors no recent sessions when the window has none.

View-only: no cell takes input. Once open the keyboard drives it — arrow keys move a highlight between cells, Enter jumps into the highlighted session and focuses that exact pane (then closes the grid), and Esc closes it. --font-size N sets an absolute cell font in points (finite, positive); --auto-size sizes cells relative to the Settings default font, shrinking as the grid grows; the two are mutually exclusive, and omitting both leaves each pane's own font untouched.

The GUI openers — ⌘⇧D, Navigate ▸ Dashboard, and the command palette's Dashboard entry — toggle the frontmost window's most-recently-used grid auto-sized (the dashboard --mru --auto-size equivalent; no separate control command). Read back from the tree's top-level dashboardMembers (the pane refs shown, in grid order — a split session appears as both <id>:left and <id>:right), dashboardHighlighted (the highlighted cell's pane ref), dashboardFontSize (the applied absolute size, omitted when untouched), and dashboardFontMode (auto/fixed/untouched).

pick

A caller-supplied fuzzy picker rendered by agterm. One picker may be pending in each window.

agtermctl pick [--prompt TEXT] [--query TEXT] [--allow-custom] [--follow] [--window W] [--no-block]
pick.open

Read choices from stdin and open the target window's native picker. Nonblank input lines become items whose id equals the label. Input whose first non-whitespace byte is [ is a JSON array of {id,label,subtitle?} objects. Item ids must be unique, labels must not be empty, and the list is capped at 1,000 items. The list may be empty only with --allow-custom, which turns the picker into a plain text prompt.

Typing matches item labels only; a subtitle is displayed but never searched. An empty query lists the items in the order the caller supplied them, so the first item is the one Return runs on open.

--prompt sets the query field placeholder. --query prefills it and filters on open, which ranks by match score and so does not preserve the supplied order. --allow-custom accepts a nonmatching query. A background --window target stays in the background unless --follow raises it. The default blocks and prints a bare picked, custom, or cancelled JSON result. --no-block prints {"id":"…"} immediately.

Tree read-back: the target tree's top-level pickPending carries the returned picker id while it waits and is omitted after resolution.

Errors: pick.open requires items (none supplied), pick.open requires at least one item (empty list without --allow-custom), too many items (max 1000), pick item label must not be empty, pick item ids must be unique, or item text must not contain control characters. A second live picker returns pick already pending; an unavailable target returns the standard window-resolution error, no open window, or no pick surface.

agtermctl pick result <id> [--window W]
pick.result

Read one picker by its globally unique exact id. Without --window, lookup remains pinned to its owning window even if the frontmost window changes; an explicit window must match. Prints bare JSON with result set to pending, picked, custom, or cancelled. Picked and custom exit 0, pending exits 1, and cancelled exits 2. A wrong id returns unknown pick: <id>.

Tree read-back: pickPending equals <id> while the result is pending and disappears for every terminal result.

agtermctl pick cancel <id> [--window W]
pick.cancel

Cancel the globally unique exact picker id; an explicit --window must match its owner. The terminal result becomes {"result":"cancelled"}; cancelling an already completed matching picker is a successful no-op, and an unknown id returns unknown pick: <id>.

Tree read-back: cancellation removes pickPending from the target tree.

quick

The frontmost window's quick terminal — a single scratch terminal at 90% of the window, not in the tree, whose shell stays alive across hides. All three are frontmost-window-only: no --target, --window, or --pane.

agtermctl quick [show|hide|toggle]
quick

Show, hide, or toggle it. Delta-computed, so it is idempotent. Errors no open window when none is open. Read its visibility back from the tree's top-level quickVisible.

agtermctl quick type <text> [--stdin]
quick.type

Inject literal keystrokes into the quick terminal — the twin of session type. It polls briefly for the surface, so quick show; quick type back-to-back is reliable. Typing into a shown-then-hidden quick terminal still works.

Errors: quick terminal not open (never shown), quick terminal not realized, no open window.

agtermctl quick text [--all] [--lines N]
quick.text

Print the quick-terminal buffer as plain text — the read-back for quick type. It does not touch the system clipboard. --all and --lines N are mutually exclusive. Polls for the surface like quick type.

notify

agtermctl notify <body> [--title T] [--target T] [--window W]
notify

Post a macOS desktop notification attributed to a session (default: the active session of the frontmost window). --title defaults to the session name. Clicking the banner reveals that session. It raises the session's unseen badge; clear it with session seen. Control-native.

The banner is gated by Settings ▸ Notifications ▸ Show notification banners; the badge is not. With banners off the command still succeeds and still raises the badge, but nothing reaches macOS — so it answers ok with an advisory result.text instead of a bare ok. A delivered notification carries none, so treat its presence as "no banner appeared". Read back: unseen on the tree node.

For agentic attention — waiting on input, or a finished result — prefer session status. A notification is a one-shot banner with no lasting state, while a status is typed and persistent, and drives the attention list, the title-bar bell, and attention navigation. Keep notify for a one-off nudge.

font

agtermctl font inc|dec|reset [--target T] [--window W]
font.inc · font.dec · font.reset

Increase, decrease, or reset the font size on the target session's surface — three separate commands sharing one CLI subcommand. The per-session zoom is persisted. The GUI half is ⌘+ / ⌘− / ⌘0.

theme

App-global — no --window. The out-of-the-box default is the bundled agterm theme; a separate default ghostty entry means "no theme" — ghostty's own built-in colors.

agtermctl theme list
theme.list

Returns result.themes (the bundled names), result.theme (the current plain theme; absent means ghostty's built-in), and result.sync with result.light/result.dark. While syncing, result.theme is absent — the state rides the three sync fields.

agtermctl theme set [name] [--light NAME] [--dark NAME|none]
theme.set

Set and persist the terminal theme app-wide, per slot — the same change as Settings ▸ Appearance. A positional name (or --light, its alias) sets the light/single theme, keeping a dark theme if one is set. Omit the name for ghostty's built-in default — with a dark theme set, that clears both.

--dark NAME sets the dark theme and turns on appearance syncing: the terminal then tracks the macOS Light/Dark setting, applying the matching side automatically. --dark none clears it, stopping the tracking. The response always echoes the full state. An unknown name errors; a positional name combined with --light is a usage error. Over the socket this is the commit — there is no live preview.

keymap

agtermctl keymap reload
keymap.reload

Re-read and apply keymap.conf. Returns result.count — the number of parse diagnostics (0 is a clean reload). App-global; the same path as File ▸ Reload Keymap. See Customizing keys for the file format.

agtermctl keymap list
keymap.list

Show the resolved keymap and the live menu key equivalents. Returns result.keymap with path, actions (every built-in with the chord it resolved to, marked when a map line moved it off its default), commands, diagnostics (line and message, not just the count), and menu — the key equivalents the menu bar carries, nested submenus included, each with its selector and marked enabled: false when the item is disabled and its chord therefore inert. The action and menu lists can disagree, which is how you tell a keymap problem from a menu one when a binding will not fire. App-global; takes no target or arguments.

config

agtermctl config reload
config.reload

Re-read and apply the ghostty config. Returns result.count — the ghostty config-diagnostic count (0 is clean). App-global; the same path as File ▸ Reload Config.

The count spans all config sources, not just the agterm-scoped ghostty.conf — libghostty diagnostics do not record which file they came from — so do not read a non-zero count as proof that ghostty.conf is the culprit.

restore

agtermctl restore clear
restore.clear

Clear every session's saved captured foreground command and persist, so the next restart restores plain shells for those panes. App-global; prints ok.

It does not clear a session new --command session's own command — that is the durable creation identity and still re-runs on restore when the setting is on. This is the counterpart to the opt-in Restore running commands on restart setting.

Errors

Every failure comes back as {"ok": false, "error": "…"} with a non-zero exit code. An unknown command fails to decode and returns a structured error — never a crash.

notFound (target resolution) ambiguous (target resolution) no such session session not realized invalid split mode invalid scratch mode session has no split session has no scratch terminal no selection (copy) overlay already open no overlay overlay still running no overlay result pane overlay already open pane not visible (overlay) no hud (hud update/close) no overlay result: the slot holds a hud a hud is always floating: pass --size-percent, not --full hud text must not contain control characters hud message too long (max 256 characters) hud detail too long (max 256 characters) session.hud.open requires a message session.hud.open: --size-percent must be 1...100 hud helper is not bundled in this build could not write the hud message invalid position: <value> (hud) invalid spinner: <value> (hud) invalid flag mode invalid fit invalid position invalid opacity invalid color text too long unsupported image (PNG or JPEG only) no such image file invalid background mode invalid sidebar mode invalid focus mode no open window quick terminal not open quick terminal not realized failed to read surface buffer window not open unknown theme: <name> unknown sound: <name> invalid color (expected #rrggbb) invalid shape: <value> (circle|square|triangle|diamond|capsule|star) invalid restore mode (session restore) session.restore set requires a command command must not contain control characters command too long (max 1024 bytes) the scratch terminal is never restored unknown pane id: <token> failed to save the restore override --pane must be left, right, or scratch
← agterm.com
GitHub Issues Discussions