Driving agterm
agterm is a native macOS terminal. It exposes a programmatic control channel over a local unix
socket, driven by the companion CLI agtermctl. Use it to build and steer terminal layouts, run
programs in overlays, type into sessions, notify the user in the exact session you are working in,
and subscribe to control events. Events cover status, notifications, session lifecycle, split and
scratch pane visibility, and structural tree changes; hooks.conf runs a shell line on any of them. They do not stream terminal output; use session text to read a buffer.
Am I inside agterm?
Each shell agterm spawns gets these environment variables. Check AGTERM_ENABLED before assuming
the control channel is available:
AGTERM_ENABLED=1 — this shell runs inside agterm.
AGTERM_SESSION_ID — the current session's UUID (the session this shell belongs to).
AGTERM_WINDOW_ID / AGTERM_WORKSPACE_ID — the owning window / workspace UUIDs.
AGTERM_SOCKET — the absolute path to the control socket this app bound.
AGTERM_PANE / AGTERM_PANE_ID: the surface's spawn role (left|right|scratch) and stable
per-surface token. The role is not rewritten after promotion or swap; the token resolves the LIVE slot.
Prefer --pane-id "$AGTERM_PANE_ID" where supported, including session status, session restore and
session text. The agent-status hook forwards both values for compatibility.
TERM_PROGRAM=agterm / TERM_PROGRAM_VERSION (agterm's version): the terminal identity, replacing
the ghostty pair embedded libghostty would set. A tool that decides a capability from a list of
terminal names (Claude Code's OSC 8 hyperlinks) needs its own override; see troubleshooting.md.
The quick terminal is scratch (not in the tree) and belongs to no window, so of the AGTERM_* variables
it only gets AGTERM_ENABLED and AGTERM_SOCKET (no session/workspace/window ids). An untargeted agtermctl run
from it therefore resolves the active window like any other caller.
These variables are inherited by every process the session's shell spawns — including long-lived
daemons that outlive the shell. A tmux/screen server, a session manager (agent-deck and the like), or
any background service started from inside a session captures the spawning session's AGTERM_* and
passes it to every child it ever creates, so status hooks running in those children resolve
$AGTERM_SESSION_ID to the session that happened to start the daemon and report to the WRONG session.
Before starting such a process from inside agterm, scrub the variables
(env -u AGTERM_ENABLED -u AGTERM_PANE -u AGTERM_PANE_ID -u AGTERM_SESSION_ID -u AGTERM_SOCKET -u AGTERM_WINDOW_ID -u AGTERM_WORKSPACE_ID <cmd>);
see troubleshooting.md ("agent-status glyph updates the wrong session") for diagnosing and fixing an
already-poisoned tmux server.
Running agtermctl
agtermctl must be on PATH (install it from agterm's Help ▸ Install Command Line Tool…). If it
is not on PATH, the user can install it, or you invoke it by absolute path.
- The socket path auto-resolves; usually no
--socket is needed. To be explicit, pass
--socket "$AGTERM_SOCKET".
--socket and other options go after the subcommand: agtermctl tree --json, not
agtermctl --json tree.
- Add
--json to any command to get the raw JSON response (machine-readable). Without it, ordinary
mutations print ok, batch close/move prints the affected session count, and tree/list print a
human listing.
- Commands other than
events make one request per invocation. events polls with a fresh connection
for each request. Mutating commands return the affected/new id; batch session mutations return the
number actually changed. Create commands (session new, session duplicate, workspace new,
window new) print the new id.
The model
A window is the top level: a named bundle rendered in its own on-screen macOS window. Each window
holds a tree of workspaces, each holding sessions. A session has a primary shell and can also
have: a split pane (a second shell side by side), a scratch terminal (a third full-coverage
shell, toggled like the split), and an ephemeral overlay (runs one program on top, then vanishes).
An overlay covers the whole session, or with --pane left|right exactly one split pane, leaving
the sibling pane visible and usable. The same session-wide slot also holds a HUD
(session hud), a small passive panel carrying a message instead of a program. A HUD can use the
whole session or one pane as its placement bounds. The session keeps focus and stays typable
under it.
One slot, so a session shows either a HUD or a program overlay, never both. Separately, the app has one
quick terminal (a scratch shell in a floating panel at 90% of the focused screen capped at 1100x700,
or whatever share Settings sets instead; not part of the tree and not owned by a window).
Inspect the live tree any time with agtermctl tree --json (workspaces → sessions, each with
id, name, cwd, splitCwd, title, active, split, overlay, hud, ask, scratch, status, background, surfaces). title is the raw OSC
terminal title (e.g. a remote host over SSH), omitted when none was reported — read it when a
session's local cwd is stale because it's connected to a remote. splitCwd is the split pane's last
reported directory, falling back to its restored directory, then the primary cwd. It is present for a
shown or hidden split and omitted without one or on older servers. surfaces[].id is the
control address for surface zoom and surface cursor (left, right, scratch, overlay,
overlay-left, or overlay-right), including hidden-but-alive split/scratch surfaces. The tree object also carries
read-only top-level fields — idleMs (ms since the last user input in the window), autoFollowMs
(the Auto-follow timeout in ms, omitted when Disabled), sidebarVisible (whether the window's
sidebar is currently shown — the read side of the write-only sidebar command), sidebarMode
(tree or flagged — the read side of sidebar mode), sidebarWidth (the sidebar divider position in
points — the read side of sidebar width, on tree only), workspaceFilter, quickVisible (whether the
quick terminal is shown — the read side of the write-only quick command; app-level, so every window
reports the same value), zoomedSurface, the four dashboard* fields, pickPending, askPending (GUI asks only), and app (the
serving app's version, plus commit when the build recorded one — the same value agtermctl version
returns). reference.md lists every one with its exact shape. List windows with
agtermctl window list --json; each window also reports autoFollowMs, sidebarVisible, geometry
(the live frame {x, y, width, height, display} in the units window move/window resize take — the
read side, so record it then restore the exact frame), and fullscreen/zoomed/minimized (the read side
of window fullscreen/window zoom/window minimize, so a script can act idempotently) — all omitted for
a closed window, but not the live idleMs, which is tree-only. A MINIMIZED window still reports its
geometry (the frame it comes back to), so a re-align script can include it.
Addressing
Commands that target a session or workspace take --target (default active):
active — the selected session / current workspace.
- a full UUID (case-insensitive), or a unique prefix of one (git-style). Zero matches →
notFound
error; two or more → ambiguous error listing candidates.
window.* commands take the window id/prefix/active as a positional argument. Other commands accept
a global --window <id|prefix|active> to operate on a specific window's tree (default: the frontmost).
Scripts rarely type ids: create with *.new (capture the returned id), or act on active.
Agents: active is almost never your own session. active is the session the USER has selected in
the GUI; your shell runs in $AGTERM_SESSION_ID, and the user is usually on a different session while
you work. For any session-scoped command meant to act on this session — session overlay open,
session scratch, session type, session text, session background, session status, session copy,
… — pass --target "$AGTERM_SESSION_ID". Omit it and
you open overlays / type into whatever the user has selected, not your own session.
Restore modes
Settings ▸ General ▸ Restore sessions is global and takes effect after restarting agterm:
- Fresh shells restores the saved windows, workspaces, sessions, directories, and split layout with new shells.
- Re-run commands starts each captured foreground command again. It does not reconnect to the old process.
- Live sessions runs every primary and split pane through zmx and reattaches to the same process. It requires
zsh as the macOS login shell. Scratch, overlay, and quick terminals stay temporary.
On a clean quit, agterm leaves live daemons running and captures each open pane's foreground command as a
fallback. A surviving daemon ignores that payload on the next launch. If an orderly machine restart removed
the daemon, zmx creates it with the captured command and the pane remains live. A pane starts a fresh shell
instead when its window was closed before quit, a hard power loss or force quit skipped capture, or the
command is denylisted or carries a control character. SIGTERM leaves live daemons running but skips capture.
tree --json is the only backing indicator: primary and split entries report surfaces[].backedByZmx, and
the session-level backedByZmx is true only when every existing primary or split is backed. The sidebar has
no zmx glyph.
Switching to Fresh shells or Re-run commands and restarting ends every detached live process in the state
directory. A launch that still requests Live sessions but cannot use it preserves those processes.
Reattach keeps usable text, TUI state, and normal colors. It does not retain inline images, earlier OSC 133
prompt markers, program-changed palette entries, or hyperlink metadata already attached to cells. New output
after reattach behaves normally.
Launching a program in a session
Bind it at creation. session new --command (and scratch --command) makes the program the session
process, so no shell line is involved:
agtermctl session new --cwd ~/proj --name worker \
--command "zsh -lc 'claude \"\$(cat ~/brief.md)\"'" # GUI PATH: wrap a non-default binary
In Fresh shells and Re-run commands modes, the session closes when this command exits unless --wait holds
the final output. In Live sessions mode the command is a create-only zmx payload, which bypasses the 1,024-byte
PTY input cap. A surviving daemon ignores the payload; a new daemon runs it, then starts the persistent shell.
The shell stays open after it exits and --wait adds no hold prompt. After a clean quit, a missing daemon
replays the captured running command inside a new persistent shell. The exclusions above start a fresh shell.
session type drives an ALREADY-RUNNING program — it is not a launcher. Its keystrokes land in a line
buffer you do not own: a newline submits (a multi-line brief becomes N premature Enters), and the user
or a concurrent agent writes to that same buffer. An untargeted session type from another agent hits
whatever is active, and session new focuses — so a just-created session is briefly active, a stray
prompt concatenates with yours, and the program starts on the merged line. (--no-select skips the
focus, but the newline and shared-buffer hazards of type-as-launcher remain — --command is still the
rule.) After --command, confirm in tree --json that the new node's foreground shows your program running, not a bare shell prompt.
Command summary
Run agtermctl <area> <cmd> --help for exact flags. Full detail in reference.md; worked
examples in examples.md; installable community workflows in cookbook.md.
tree — print the workspace/session tree (--json for structured). Each session node carries
foreground/splitForeground (the live argv of each pane's foreground process, omitted when the pane
is at its shell prompt, or running a setuid/setgid program like top or sudo whose argv macOS won't
expose) — i.e. what each pane is currently running — foregroundShell/splitForegroundShell (the shell
holding each pane's foreground as a basename, present exactly when that pane's foreground is omitted
because a shell holds it, so an EXISTING pane with neither is one whose process could not be read; check
hasSplit before reading the split pair. Not a claim the pane is at a prompt and never permission to type —
a builtin like read runs inside the shell), restoreCommand/splitRestoreCommand (each pane's
persisted restore-command override set via session restore — the read side: omitted = auto-capture, ""
= pinned to nothing (a plain shell), a command = the shell line that runs on the next launch), status (the agent-status set
via session status: active|completed|blocked, omitted when idle), statusPane (which pane set
that status: left (main) | right (split) | scratch, from session status --pane, omitted when
unset or idle), statusBlink/statusColor/statusShape (the status glyph's --blink flag, its --color
#rrggbb tint and its --shape silhouette from session status, omitted when idle / not blinking / using
the configured color or shape — the tint and the silhouette report the per-call override only),
statusChangedAt (when that status was last set, in epoch seconds — the same clock as an event's ts;
omitted when idle, and refreshed by a re-push of the SAME status, so now - statusChangedAt is how long
ago the status was last written — normally the agent's own push, though a pane promotion re-tags the
indicator and counts too; ephemeral, so it does not survive a restart), background (the background
spec — image/text watermark or solid color — set via session background, omitted when none — the read side of set/clear),
unseen (the unseen-notification badge count — raised by notify/OSC 9/777, cleared by session seen; omitted when zero), commandWait/splitCommandWait (whether either pane's --command was
created with --wait to hold open after exit, the read side of session new --wait; each omitted for a
plain or non-holding pane), overlaySizePercent (an open overlay's floating-panel percent 1-100,
omitted for a full-pane overlay or no overlay so gate on overlay first; the read side of session overlay resize for a record-then-restore zoom), paneOverlays (the panes covered by their own overlay —
["left"], ["right"] or ["left","right"], omitted when neither is; the read side of session overlay open --pane, independent of the session-wide overlay flag),
hud (the message panel occupying the session-wide slot — {message, detail?, spinner, backgroundColor?, textColor?, sizePercent?, heightPercent?, position, pane?}, the two percents being the panel's width and height
shares — omitted when none is up; the read side of session hud. position and spinner
always report the EFFECTIVE value, center and a static panel's none included, so a caller who omitted
them never has to know the defaults; spinner names the STYLE, so none is what a caller echoes back to
turn one off. While a HUD is up the node's overlay reads false and overlaySizePercent is omitted, so a
poll for "is a program covering this session" cannot mistake a message for one; HUD state is poll-only,
no event announces it),
realized (whether the session's MAIN pane has a live terminal; false means no shell was spawned.
session text then answers session not realized without realizing anything; session type brings up a
restored main pane still waiting its turn in a launch that replays commands, while any other unrealized
cause can still exhaust its poll and fail the same way. session new returns ok for a model
entry, which is weaker — libghostty will not create a surface while the display is asleep, so a session
created by a scheduled job overnight stays unrealized until the displays wake and then recovers itself.
Poll this after an unattended create),
backedByZmx (true only when every existing primary/split pane is currently zmx-backed; primary/split
entries in surfaces report their own Boolean, while scratch and overlays omit it),
liveAttribution and splitLiveAttribution (local Live pane attribution, including hidden splits;
values and omission rules),
remoteHost (the machine an attached session came from, the read side of zmx attach; omitted for a local
session, and never present after a relaunch because a remote session is not persisted),
hasSplit (whether a second pane exists at all, shown or hidden; omitted when there is none — read this
rather than split, which is false for a split hidden with ⌘D even though its pane is still alive),
splitAxis (vertical for left/right or horizontal for top/bottom; omitted without a split),
splitRatio (the primary-pane divider fraction 0.05-0.95 of the area below the titlebar, of a
session that has a split — shown or hidden; omitted when there's no split, or while the split has never
been shown — a shown split always reports a value, 0.5 when nothing set one) —
the read side of session resize, record it to restore the exact divider), splitFocused
(which pane holds focus in a session that has a split: true = split/right/bottom, false = primary/left/top; omitted
when there's no split; the read side of session focus, record it to restore focus), and surfaces
(id, kind, active, visible, and backedByZmx on primary/split entries) for surface zoom and
surface cursor. The tree top level carries zoomedSurface
(the control id of the currently zoomed surface, omitted when nothing is zoomed — the read side of
surface zoom, so a script can check the zoom state and record-then-restore). It also carries the read
side of the dashboard command (all omitted when no dashboard is open): dashboardMembers (the pane refs
the open dashboard shows, in grid order — <session-id>:left for a primary pane, <session-id>:right for
a split pane, so a split session appears as both), dashboardHighlighted (the highlighted cell's pane ref —
the one Enter jumps into, focusing that exact pane), dashboardFontSize (the absolute font size in points
applied to the cells, omitted when untouched), and dashboardFontMode (auto|fixed|untouched).
The top level also carries pickPending, the id of the native picker currently awaiting an answer in
that window, omitted when no pick is pending.
events: continuously print control events, subscribing from the current tail when no cursor is
given. Use --json for one bare event object per line; filter with repeatable or comma-separated
--kind status|notify|session.created|session.closed|tree.changed|pane.split|pane.scratch; resume with paired
--run RUN --after SEQ; and set page size with --limit 1...1000. The app retains 4,096 events for
one process run. Cursor run changes, expiry, and ahead-of-tail errors are fatal and are never silently
rebaselined. There is no terminal-output event stream.
workspace — workspace new [name] [--collapsed] (--collapsed creates it closed in the sidebar so you can fill
it with session new --no-select without it opening, and keeps it out of the focus set; a plain create
joins the marked set while the filter is applied, so it is visible) · workspace rename <name> ·
workspace delete · workspace select ·
workspace go --to next|prev (step the CURRENT workspace one place through the sidebar's visible order, wrapping,
and select the first session of the one it lands on — relative, so no --target, and unaffected by
whether a workspace is collapsed; workspace move REORDERS instead) ·
workspace move --to up|down|top|bottom ·
workspace focus [on|off|toggle|add] (mark ONE workspace in the sidebar's focus set — on marks it alone and
applies the filter, off unmarks it, toggle (default) replace-toggles, and add marks it alongside
the others WITHOUT switching the filter on; read membership back from the tree workspace node's
focused flag) ·
workspace filter [on|off|toggle] (apply or suspend that filter for the whole window WITHOUT losing the marked
set — no --target; read it back from the tree top-level workspaceFilter. Build a working set with
repeated workspace focus add, then apply it once with workspace filter on; a workspace row renders iff
sidebarVisible && sidebarMode == "tree" && (!workspaceFilter || focused) — no workspace row renders at
all with the sidebar hidden or in flagged mode, the whole tree renders while the filter is off, and
only while it is on does visibility narrow to the members — and workspace filter on with nothing marked is
refused so the pair can never lie) ·
workspace collapse [--target W] [--window W] · workspace expand [--target W] [--window W] (collapse/expand ONE workspace
in the sidebar tree — the per-workspace pair, distinct from the all-workspace sidebar expand/collapse;
read the open/closed state back from the tree workspace node's collapsed flag, true when collapsed and
omitted when expanded).
session
session new [--cwd DIR] [--workspace W] [--workspace-name NAME] [--create-workspace] [--command CMD] [--wait] [--name NAME] [--after SID | --before SID] [--no-select] —
create (and focus) a session. Target the workspace by id/prefix (--workspace) OR by name
(--workspace-name, mutually exclusive); add --create-workspace to reuse-or-create the named
workspace when absent. --command runs that program as the session process instead of a login shell
(argv-only, and with the app's GUI PATH — a Homebrew/non-default binary needs an absolute path or a
zsh -lc '…' wrapper, else exit 127; same caveat for session scratch --command and session overlay open below);
--wait (with --command, else an error) HOLDS the session open after the command exits, showing the
press-any-key prompt with the final output intact instead of closing (persists across restart, unlike an
overlay's live-only wait; read back on tree's commandWait);
--name seeds the sidebar label (default: the auto basename). --after/--before place it directly
after/before an anchor session (id/prefix/active) instead of appending — the anchor carries its own
workspace, so it's mutually exclusive with --workspace/--workspace-name. new --after active =
create right after the current session. --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); omit it for the default select-and-focus behavior.
session duplicate [--target] — create a fresh session (a plain login shell) in the target's workspace, right
after it, rooted at the target's focused-pane cwd; selects + focuses it and returns the new id. ONLY the
directory carries over — no custom name, command, split, scratch, status, flag, font size, or background.
Equivalent to session new --cwd <source cwd> --after <source> in one round-trip, except that a remote
source's cwd goes through the local rule first (an existing local directory is kept, anything else
becomes home). Read it back from tree: the new node sits 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, or a remote session, where it can read as
home).
session close [--target T ...] — close one session, or repeat --target to close a batch with one
grace-period undo.
session select · session rename <name> · session reveal (select the focused pane's cwd in Finder).
session go --to next|prev|first|last|next-attention|prev-attention — move the selection between sessions.
session move <workspace> (relocate) or session move --to up|down|top|bottom (reorder within the
workspace) or session move --after SID | --before SID (place after/before an anchor session; the anchor carries its own
workspace, so this relocates + positions in one shot, even cross-workspace). For workspace and
after/before placement, repeat --target to move several sessions as one ordered block. Do not repeat
--target with --to up|down|top|bottom.
- Shared pane selectors accept
primary/left/top for the primary pane and
split/right/bottom for the split pane. Commands supporting scratch also accept scratch.
Syntax and read-back use canonical left/right/scratch; the invalid-value error keeps those names.
session type <text> [--stdin] [--select] [--pane left|right|scratch] — inject keystrokes (real typing, Enter
included) into the main pane, the split pane with --pane right, or the scratch terminal (even hidden)
with --pane scratch. Pass --target "$AGTERM_SESSION_ID" to type into YOUR session, not the user's
active one (see Addressing). Like session text, every --pane addresses the surface UNDER a covering
overlay — by design, so a pane stays drivable whatever is drawn over it — meaning text typed while one is
open runs in the hidden shell and is invisible until it closes. There is no write twin of
session overlay text: an overlay runs the caller's own program, so nothing types into one. Typing is the
input a waiting agent asked for, so it clears that pane's blocked/completed glyph exactly as a
keystroke does, under Settings ▸ Agent Status ▸ Status reset: on the first key by default, only when the
text carries a newline under On Enter, never when Disabled; another pane's glyph, an active one, and an
empty payload are left alone.
session copy — print the session's selected text (does NOT touch the system clipboard).
session paste — paste the system clipboard into the session (the socket analogue of ⌘V; read it back with
session text). --pane left|right|scratch picks the pane, with the usual role and position aliases;
omitted is the main pane.
session select-all — select the session's entire terminal buffer (the socket analogue of ⌘A; read the
selection back with session copy).
session text [--all] [--lines N] [--pane left|right|scratch] [--pane-id TOKEN]: print the session buffer
as plain text. Default is the visible screen of the focused pane; --pane scratch reads the scratch
terminal even while hidden; --pane-id "$AGTERM_PANE_ID" follows the same terminal after a role change
and overrides --pane when it resolves; --all adds available scrollback (alternate-screen buffers
have none); --lines N keeps the last N lines.
session search [needle] [--next|--prev|--close] — search the terminal scrollback; prints the "N of M" counter.
session split [on|off|toggle] [--axis vertical|horizontal] · session split close - second shell, left/right by
default or top/bottom with --axis horizontal. Omitting --axis preserves the current axis and the
legacy left/right behavior. The GUI actions are ⌘D for vertical and ⌘⇧D for horizontal; either
transposes a shown split of the other orientation. Hide keeps it alive; close destroys the pane and
whatever runs in it.
session swap: exchange the two terminals' physical positions and primary/split roles without restarting
them. Focus follows the terminal; axis and divider ratio stay fixed. Works on shown or hidden splits and
under zoom/dashboard; errors when there is no split or either surface is not ready. Read the new primary
from tree's cwd/title/foreground and the other side from splitCwd/splitForeground.
session scratch [on|off|toggle] [--command CMD] — full-coverage third shell (hide keeps it alive; exit
recreates). --command (when showing) runs a program instead of a shell, run-once like session new --command (respawns the scratch if one is open). Target your own session with
--target "$AGTERM_SESSION_ID" (see Addressing).
session focus [primary|split|left|right|top|bottom|other] - move focus between split panes. Role and position
aliases select the same two live terminals; readback remains left/right.
session resize --split-ratio R | --grow-left D | --grow-right D | --grow-primary D | --grow-split D | --grow-top D | --grow-bottom D - move the split divider (the GUI only drags
it, or double-clicks it for an even split; bind any other fraction via a
command "agtermctl session resize …" custom action). --split-ratio sets
the absolute primary-pane fraction of the area below the titlebar (left or top; 0..1, clamped to
0.05..0.95). The grow options are
aliases for growing the primary or split pane. Prints the applied fraction.
session status <idle|active|completed|blocked> [--blink] [--auto-reset] [--sound NAME] [--color #rrggbb] [--shape SHAPE] [--pane left|right|scratch] [--pane-id TOKEN] — set the sidebar agent glyph (--sound default or a system sound name plays a one-shot sound; --color tints the glyph for this call only, reverting on the next status set without it; --shape (circle, square, triangle, diamond, capsule, star) picks its silhouette for this call only and reverts the same way, read back as the tree statusShape field; --pane records which pane set it — left=main, right=split, scratch — so foreground typing in another pane won't clear it, and while the session is blocked a status from another pane that is not itself blocked is refused with blocked status owned by pane <pane> so one pane's agent cannot erase the other's request for input, idle included since the bundled hooks emit it unprompted; any user-initiated GUI selection (auto-follow, attention-nav ⌃⌥↑/↓, plain session nav, the command palettes, a Dock-menu session, a sidebar row click) reveals that pane when the status needs attention (blocked/completed); active preserves the existing pane selection; the pane reads back as the tree statusPane field; the socket session go next-attention only steps the selection, it does not itself reveal the pane; --pane-id is the hook-forwarded stable surface token ($AGTERM_PANE_ID) that resolves the pane's live slot and overrides a stale --pane after a promote + re-split — scripts set --pane directly and leave --pane-id to the hook).
session flag [on|off|toggle|clear] — flag a session for the flagged working-set view (clear unflags all).
session context <TEXT|--clear> [--target] [--window W] — set what the session is ABOUT, shown in the
title bar: a PR number, an issue, the task in hand. Use it when you
start work a session's name cannot describe. Exactly one of TEXT or --clear; a blank TEXT is an error,
not a second way to clear. Trimmed; max 256 UTF-8 bytes; no control characters (tabs included) or line
breaks. Persists
across a relaunch until cleared. Read it back from the tree node's context field.
session seen [--target] [--window W] — clear the session's unseen-notification badge WITHOUT changing the
selection or focus (the focus-free counterpart to notify, which raises the badge). Idempotent — a
no-op when already zero. Read the current count from the tree node's unseen field. Use it so an
orchestrator can acknowledge a driven session's notifications without pulling focus to it.
session restore ("cmd" | --none | --clear) [--pane left|right] [--pane-id TOKEN] — pin what a pane re-runs on
the NEXT launch, overriding the captured foreground command. A "cmd" shell line pins it, --none pins
nothing (a plain shell), --clear drops the override back to auto-capture. Written now, consumed on the
next launch (it never touches the running session), and STICKY — fires again on every restart until
cleared. It runs in rerun mode. In fresh-shell or live mode, a command or --none still saves policy
for a future rerun launch and returns a note naming the active mode; --clear works in every mode. A pin
never opts one session out of live mode. Deliberate pins bypass restore-denylist.conf. Read back as the tree node's
restoreCommand/splitRestoreCommand. --pane right needs a split; scratch is rejected. --pane-id
(the shell's $AGTERM_PANE_ID) resolves the pane's live slot — unlike session status, a token that
does not resolve errors unless --pane is also given. For a non-idempotent command like
claude --resume … --fork-session (which mints a new session on every restart), a Claude Code
SessionStart hook rewrites the override to the live id on every start so the next restart reattaches
instead of forking. The pinned value is shell code stored in the state file and readable via tree, so
it must not carry secrets. See examples.md.
session background image <path> [--opacity F] [--fit contain|cover|stretch|none] [--position P] [--repeat] ·
session background text <text> [--color #rrggbb] [--opacity F] [--fit ...] [--position ...] ·
session background color <#rrggbb> · session background clear — composite an image (PNG/JPEG) or rasterized text
behind the terminal as a watermark (auto-fitting the window, re-fits on resize), or set a solid
terminal background color. Per session; survives restart. --opacity 0.0–1.0. (An image/text watermark
renders the pane opaque, overriding window translucency, so it shows; a color takes no opacity and
honors the Settings window translucency instead.)
session overlay open <command> [--cwd DIR] [--wait] [--block] [--size-percent N] [--background-color #rrggbb] [--follow] [--pane left|right] ·
session overlay resize (--size-percent N | --full) ·
session overlay close [--pane left|right] ·
session overlay result [--pane left|right] ·
session overlay copy [--pane left|right] ·
session overlay text [--all] [--lines N] [--pane left|right] — run a program on top of a session; --block
waits and exits with its status.
session overlay copy returns the selection made INSIDE the overlay and session overlay text its terminal buffer:
session copy and session text both address the pane the overlay COVERS, so a selection made in the
overlay reads there as no selection and session text --pane right returns the shell underneath.
Reach for them when the read is NOT chord-driven — polling from outside, or reading some time after the
fact. A chord already gets the firing surface's selection synchronously in $AGT_SELECTION, the
overlay's included, so a custom command should use that rather than a later socket read.
session overlay text returns a TUI's drawn screen wrapped as rendered — for a program's OUTPUT, still prefer
its own output file.
session overlay resize changes an ALREADY-OPEN overlay: --size-percent N (1-100) makes it a floating panel,
--full switches it back to the full-pane overlay; the program keeps running (no re-spawn).
--pane left|right scopes the overlay to ONE split pane instead of the whole session, leaving the
sibling pane live and interactive; left and right are independent and may both be open at once. A pane
overlay is ALWAYS full-pane, so --pane cannot combine with --size-percent and session overlay resize
takes no --pane. Everything else is identical to the session-wide overlay. A non-split session
accepts --pane left. AGTERM_PANE is only the shell's spawn role and may be stale after promotion or
session swap, so a long-running shell must not assume --pane "$AGTERM_PANE" still names its slot.
A pane that is not currently rendered is refused with pane not visible. A SHOWN
split renders both panes, a HIDDEN one renders only the FOCUSED pane, so the refused one is the pane
that does not have focus.
Target with --target "$AGTERM_SESSION_ID" for YOUR session (default active is the user's selection).
By default session overlay open does NOT switch the user — full and floating (--size-percent N, 1-100)
both open on --target and run their program in the background; the panel appears when the user visits
that session. Pass --follow to select the target after opening (a no-op if it is already active):
use --follow when you want the user pulled to the overlay, omit it to open quietly on your own or
another session.
--background-color gives the overlay pane its own solid color, independent of the session's. An
overlay is a real terminal (pty), which is also how you display an image inline — via the bundled
scripts/show-image.sh (see below).
session hud [open] <message> [--detail T] [--spinner] [--spinner-style S] [--position P] [--background-color #rrggbb] [--text-color #rrggbb] [--size-percent N] [--pane P] [--pane-id ID] ·
session hud update <message> [--detail T] [--spinner] [--spinner-style S] [--position P] [--text-color #rrggbb] [--size-percent N] [--pane P] [--pane-id ID] ·
session hud close — post a small passive panel over the session saying what you are doing
("gathering options…"). Unlike an overlay it takes no input and steals nothing: the session keeps first
responder, the user keeps typing, and the terminal behind it is neither dimmed nor click-blocked. Use it
for the seconds an agent needs before it can show something (computing picker items, waiting on a slow
command), then take it down. open is the default subcommand, so session hud "…" posts; a message that is
literally update or close needs the explicit session hud open verb. --detail adds a dim second line,
--spinner animates a glyph in the default bar style and --spinner-style bar|braille|circle|blocks|dot
picks another, turning the spinner on by itself (dot blinks instead of animating, for a panel up for
minutes; an update may switch style in place). --spinner-style none is accepted and leaves the panel
static, so the none a read-back reports round-trips. --position anchors it 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 set session background takes; every anchor off center holds a fixed margin
off that pane edge automatically, so a corner keeps the panel out of the text the user is reading. The
bare top/bottom are still accepted for top-center/bottom-center, and the read-back reports the
canonical anchor. --pane primary|left|top|split|right|bottom makes that pane the coordinate space for
the anchor, size cap, and margin. --pane-id "$AGTERM_PANE_ID" follows the same shell after pane swaps or
promotion and overrides --pane when it resolves. An unknown token needs a --pane fallback. Open refuses
a pane that is not visible. Hiding a target keeps the HUD alive until the pane returns; closing it closes
the HUD. The panel is sized from the message on both axes:
width from the longest line, height from the number of them — so a title and a subtitle give a wide, short
panel, not a square one. --size-percent N (1-100) overrides the WIDTH only, bounded to 10-80% of the
pane, since a message must never cover the session it is about, so a requested 100 reads back as 80. The
height always follows the message. --text-color colors the panel's TEXT and --background-color its
backing, independently. session hud update repaints in place with no re-spawn and no blink,
and REPLACES the whole spec. Repeat --detail/--spinner/--text-color/--pane/--pane-id to keep them, since an omitted
one drops. It takes no --background-color: the s
…(truncated)
1---2name: agterm3description: Drive agterm, a native macOS terminal, through its agtermctl CLI and local control socket. Use when running inside an agterm session and asked to control the terminal: create, rename, close, select or reorder sessions and workspaces; split panes; toggle the scratch terminal; run a program in an overlay and read its exit status; post a HUD panel or a desktop notification; show a native picker with caller-supplied choices or a question dialog with named buttons; display an image inline; type into a session, copy its selection or search its scrollback; manage windows; change font size; set the theme; reload or edit the keymap, the event hooks and the agterm-scoped ghostty config; subscribe to status, notification, lifecycle, pane-visibility and tree-change events. Covers the window/workspace/session addressing model and the AGTERM_* environment a spawned shell sees, attaching a session running on another Mac, the cookbook recipes, the running version, and diagnosing problems or filing an agterm bug or feature re4---56<!-- agterm-skill -->78# Driving agterm910agterm is a native macOS terminal. It exposes a programmatic control channel over a local unix11socket, driven by the companion CLI `agtermctl`. Use it to build and steer terminal layouts, run12programs in overlays, type into sessions, notify the user in the exact session you are working in,13and subscribe to control events. Events cover status, notifications, session lifecycle, split and14scratch pane visibility, and structural tree changes; `hooks.conf` runs a shell line on any of them. They do not stream terminal output; use `session text` to read a buffer.1516## Am I inside agterm?1718Each shell agterm spawns gets these environment variables. Check `AGTERM_ENABLED` before assuming19the control channel is available:2021- `AGTERM_ENABLED=1` — this shell runs inside agterm.22- `AGTERM_SESSION_ID` — the current session's UUID (the session this shell belongs to).23- `AGTERM_WINDOW_ID` / `AGTERM_WORKSPACE_ID` — the owning window / workspace UUIDs.24- `AGTERM_SOCKET` — the absolute path to the control socket this app bound.25- `AGTERM_PANE` / `AGTERM_PANE_ID`: the surface's spawn role (`left`|`right`|`scratch`) and stable26 per-surface token. The role is not rewritten after promotion or swap; the token resolves the LIVE slot.27 Prefer `--pane-id "$AGTERM_PANE_ID"` where supported, including `session status`, `session restore` and28 `session text`. The agent-status hook forwards both values for compatibility.29- `TERM_PROGRAM=agterm` / `TERM_PROGRAM_VERSION` (agterm's version): the terminal identity, replacing30 the `ghostty` pair embedded libghostty would set. A tool that decides a capability from a list of31 terminal names (Claude Code's OSC 8 hyperlinks) needs its own override; see troubleshooting.md.3233The quick terminal is scratch (not in the tree) and belongs to no window, so of the `AGTERM_*` variables34it only gets `AGTERM_ENABLED` and `AGTERM_SOCKET` (no session/workspace/window ids). An untargeted `agtermctl` run35from it therefore resolves the active window like any other caller.3637These variables are inherited by every process the session's shell spawns — including long-lived38daemons that outlive the shell. A tmux/screen server, a session manager (agent-deck and the like), or39any background service started from inside a session captures the spawning session's `AGTERM_*` and40passes it to every child it ever creates, so status hooks running in those children resolve41`$AGTERM_SESSION_ID` to the session that happened to start the daemon and report to the WRONG session.42Before starting such a process from inside agterm, scrub the variables43(`env -u AGTERM_ENABLED -u AGTERM_PANE -u AGTERM_PANE_ID -u AGTERM_SESSION_ID -u AGTERM_SOCKET -u AGTERM_WINDOW_ID -u AGTERM_WORKSPACE_ID <cmd>`);44see troubleshooting.md ("agent-status glyph updates the wrong session") for diagnosing and fixing an45already-poisoned tmux server.4647## Running agtermctl4849`agtermctl` must be on PATH (install it from agterm's **Help ▸ Install Command Line Tool…**). If it50is not on PATH, the user can install it, or you invoke it by absolute path.5152- The socket path auto-resolves; usually no `--socket` is needed. To be explicit, pass53 `--socket "$AGTERM_SOCKET"`.54- `--socket` and other options go **after** the subcommand: `agtermctl tree --json`, not55 `agtermctl --json tree`.56- Add `--json` to any command to get the raw JSON response (machine-readable). Without it, ordinary57 mutations print `ok`, batch close/move prints the affected session count, and `tree`/`list` print a58 human listing.59- Commands other than `events` make one request per invocation. `events` polls with a fresh connection60 for each request. Mutating commands return the affected/new id; batch session mutations return the61 number actually changed. Create commands (`session new`, `session duplicate`, `workspace new`,62 `window new`) print the new id.6364## The model6566A **window** is the top level: a named bundle rendered in its own on-screen macOS window. Each window67holds a tree of **workspaces**, each holding **sessions**. A session has a primary shell and can also68have: a **split** pane (a second shell side by side), a **scratch** terminal (a third full-coverage69shell, toggled like the split), and an ephemeral **overlay** (runs one program on top, then vanishes).70An overlay covers the whole session, or with `--pane left|right` exactly one split pane, leaving71the sibling pane visible and usable. The same session-wide slot also holds a **HUD**72(`session hud`), a small passive panel carrying a message instead of a program. A HUD can use the73whole session or one pane as its placement bounds. The session keeps focus and stays typable74under it.75One slot, so a session shows either a HUD or a program overlay, never both. Separately, the app has one76**quick terminal** (a scratch shell in a floating panel at 90% of the focused screen capped at 1100x700,77or whatever share Settings sets instead; not part of the tree and not owned by a window).7879Inspect the live tree any time with `agtermctl tree --json` (workspaces → sessions, each with80`id`, `name`, `cwd`, `splitCwd`, `title`, `active`, `split`, `overlay`, `hud`, `ask`, `scratch`, `status`, `background`, `surfaces`). `title` is the raw OSC81terminal title (e.g. a remote host over SSH), omitted when none was reported — read it when a82session's local `cwd` is stale because it's connected to a remote. `splitCwd` is the split pane's last83reported directory, falling back to its restored directory, then the primary cwd. It is present for a84shown or hidden split and omitted without one or on older servers. `surfaces[].id` is the85control address for `surface zoom` and `surface cursor` (`left`, `right`, `scratch`, `overlay`,86`overlay-left`, or `overlay-right`), including hidden-but-alive split/scratch surfaces. The tree object also carries87read-only top-level fields — `idleMs` (ms since the last user input in the window), `autoFollowMs`88(the Auto-follow timeout in ms, omitted when Disabled), `sidebarVisible` (whether the window's89sidebar is currently shown — the read side of the write-only `sidebar` command), `sidebarMode`90(`tree` or `flagged` — the read side of `sidebar mode`), `sidebarWidth` (the sidebar divider position in91points — the read side of `sidebar width`, on `tree` only), `workspaceFilter`, `quickVisible` (whether the92quick terminal is shown — the read side of the write-only `quick` command; app-level, so every window93reports the same value), `zoomedSurface`, the four `dashboard*` fields, `pickPending`, `askPending` (GUI asks only), and `app` (the94serving app's `version`, plus `commit` when the build recorded one — the same value `agtermctl version`95returns). reference.md lists every one with its exact shape. List windows with96`agtermctl window list --json`; each window also reports `autoFollowMs`, `sidebarVisible`, `geometry`97(the live frame `{x, y, width, height, display}` in the units `window move`/`window resize` take — the98read side, so record it then restore the exact frame), and `fullscreen`/`zoomed`/`minimized` (the read side99of `window fullscreen`/`window zoom`/`window minimize`, so a script can act idempotently) — all omitted for100a closed window, but not the live `idleMs`, which is `tree`-only. A MINIMIZED window still reports its101`geometry` (the frame it comes back to), so a re-align script can include it.102103## Addressing104105Commands that target a session or workspace take `--target` (default `active`):106107- `active` — the selected session / current workspace.108- a full UUID (case-insensitive), or a unique **prefix** of one (git-style). Zero matches → `notFound`109 error; two or more → `ambiguous` error listing candidates.110111`window.*` commands take the window id/prefix/`active` as a positional argument. Other commands accept112a global `--window <id|prefix|active>` to operate on a specific window's tree (default: the frontmost).113114Scripts rarely type ids: create with `*.new` (capture the returned id), or act on `active`.115116**Agents: `active` is almost never your own session.** `active` is the session the USER has selected in117the GUI; your shell runs in `$AGTERM_SESSION_ID`, and the user is usually on a different session while118you work. For any session-scoped command meant to act on *this* session — `session overlay open`,119`session scratch`, `session type`, `session text`, `session background`, `session status`, `session copy`,120… — pass `--target "$AGTERM_SESSION_ID"`. Omit it and121you open overlays / type into whatever the user has selected, not your own session.122123## Restore modes124125**Settings ▸ General ▸ Restore sessions** is global and takes effect after restarting agterm:126127- **Fresh shells** restores the saved windows, workspaces, sessions, directories, and split layout with new shells.128- **Re-run commands** starts each captured foreground command again. It does not reconnect to the old process.129- **Live sessions** runs every primary and split pane through zmx and reattaches to the same process. It requires130 zsh as the macOS login shell. Scratch, overlay, and quick terminals stay temporary.131132On a clean quit, agterm leaves live daemons running and captures each open pane's foreground command as a133fallback. A surviving daemon ignores that payload on the next launch. If an orderly machine restart removed134the daemon, zmx creates it with the captured command and the pane remains live. A pane starts a fresh shell135instead when its window was closed before quit, a hard power loss or force quit skipped capture, or the136command is denylisted or carries a control character. SIGTERM leaves live daemons running but skips capture.137`tree --json` is the only backing indicator: primary and split entries report `surfaces[].backedByZmx`, and138the session-level `backedByZmx` is true only when every existing primary or split is backed. The sidebar has139no zmx glyph.140Switching to Fresh shells or Re-run commands and restarting ends every detached live process in the state141directory. A launch that still requests Live sessions but cannot use it preserves those processes.142143Reattach keeps usable text, TUI state, and normal colors. It does not retain inline images, earlier OSC 133144prompt markers, program-changed palette entries, or hyperlink metadata already attached to cells. New output145after reattach behaves normally.146147## Launching a program in a session148149**Bind it at creation.** `session new --command` (and `scratch --command`) makes the program the session150process, so no shell line is involved:151152```bash153agtermctl session new --cwd ~/proj --name worker \154 --command "zsh -lc 'claude \"\$(cat ~/brief.md)\"'" # GUI PATH: wrap a non-default binary155```156157In Fresh shells and Re-run commands modes, the session closes when this command exits unless `--wait` holds158the final output. In Live sessions mode the command is a create-only zmx payload, which bypasses the 1,024-byte159PTY input cap. A surviving daemon ignores the payload; a new daemon runs it, then starts the persistent shell.160The shell stays open after it exits and `--wait` adds no hold prompt. After a clean quit, a missing daemon161replays the captured running command inside a new persistent shell. The exclusions above start a fresh shell.162163`session type` drives an ALREADY-RUNNING program — it is not a launcher. Its keystrokes land in a line164buffer you do not own: a newline submits (a multi-line brief becomes N premature Enters), and the user165or a concurrent agent writes to that same buffer. An untargeted `session type` from another agent hits166whatever is `active`, and `session new` focuses — so a just-created session is briefly `active`, a stray167prompt concatenates with yours, and the program starts on the merged line. (`--no-select` skips the168focus, but the newline and shared-buffer hazards of `type`-as-launcher remain — `--command` is still the169rule.) After `--command`, confirm in `tree --json` that the new node's `foreground` shows your program running, not a bare shell prompt.170171## Command summary172173Run `agtermctl <area> <cmd> --help` for exact flags. Full detail in **reference.md**; worked174examples in **examples.md**; installable community workflows in **cookbook.md**.175176**tree** — print the workspace/session tree (`--json` for structured). Each session node carries177`foreground`/`splitForeground` (the live argv of each pane's foreground process, omitted when the pane178is at its shell prompt, or running a setuid/setgid program like `top` or `sudo` whose argv macOS won't179expose) — i.e. what each pane is currently running — `foregroundShell`/`splitForegroundShell` (the shell180holding each pane's foreground as a basename, present exactly when that pane's `foreground` is omitted181because a shell holds it, so an EXISTING pane with neither is one whose process could not be read; check182`hasSplit` before reading the split pair. Not a claim the pane is at a prompt and never permission to type —183a builtin like `read` runs inside the shell), `restoreCommand`/`splitRestoreCommand` (each pane's184persisted restore-command override set via `session restore` — the read side: omitted = auto-capture, `""`185= pinned to nothing (a plain shell), a command = the shell line that runs on the next launch), `status` (the agent-status set186via `session status`: `active`|`completed`|`blocked`, omitted when idle), `statusPane` (which pane set187that status: `left` (main) | `right` (split) | `scratch`, from `session status --pane`, omitted when188unset or idle), `statusBlink`/`statusColor`/`statusShape` (the status glyph's `--blink` flag, its `--color`189`#rrggbb` tint and its `--shape` silhouette from `session status`, omitted when idle / not blinking / using190the configured color or shape — the tint and the silhouette report the per-call override only),191`statusChangedAt` (when that status was last set, in epoch seconds — the same clock as an event's `ts`;192omitted when idle, and refreshed by a re-push of the SAME status, so `now - statusChangedAt` is how long193ago the status was last written — normally the agent's own push, though a pane promotion re-tags the194indicator and counts too; ephemeral, so it does not survive a restart), `background` (the background195spec — image/text watermark or solid color — set via `session background`, omitted when none — the read side of set/clear),196`unseen` (the unseen-notification badge count — raised by `notify`/OSC 9/777, cleared by `session197seen`; omitted when zero), `commandWait`/`splitCommandWait` (whether either pane's `--command` was198created with `--wait` to hold open after exit, the read side of `session new --wait`; each omitted for a199plain or non-holding pane), `overlaySizePercent` (an open overlay's floating-panel percent 1-100,200omitted for a full-pane overlay or no overlay so gate on `overlay` first; the read side of `session201overlay resize` for a record-then-restore zoom), `paneOverlays` (the panes covered by their own overlay —202`["left"]`, `["right"]` or `["left","right"]`, omitted when neither is; the read side of `session overlay203open --pane`, independent of the session-wide `overlay` flag),204`hud` (the message panel occupying the session-wide slot — `{message, detail?, spinner, backgroundColor?,205textColor?, sizePercent?, heightPercent?, position, pane?}`, the two percents being the panel's width and height206shares — omitted when none is up; the read side of `session hud`. `position` and `spinner`207always report the EFFECTIVE value, `center` and a static panel's `none` included, so a caller who omitted208them never has to know the defaults; `spinner` names the STYLE, so `none` is what a caller echoes back to209turn one off. While a HUD is up the node's `overlay` reads `false` and `overlaySizePercent` is omitted, so a210poll for "is a program covering this session" cannot mistake a message for one; HUD state is poll-only,211no event announces it),212`realized` (whether the session's MAIN pane has a live terminal; `false` means no shell was spawned.213`session text` then answers `session not realized` without realizing anything; `session type` brings up a214restored main pane still waiting its turn in a launch that replays commands, while any other unrealized215cause can still exhaust its poll and fail the same way. `session new` returns `ok` for a model216entry, which is weaker — libghostty will not create a surface while the display is asleep, so a session217created by a scheduled job overnight stays unrealized until the displays wake and then recovers itself.218Poll this after an unattended create),219`backedByZmx` (true only when every existing primary/split pane is currently zmx-backed; primary/split220entries in `surfaces` report their own Boolean, while scratch and overlays omit it),221`liveAttribution` and `splitLiveAttribution` (local Live pane attribution, including hidden splits;222[values and omission rules](reference.md#tree)),223`remoteHost` (the machine an attached session came from, the read side of `zmx attach`; omitted for a local224session, and never present after a relaunch because a remote session is not persisted),225`hasSplit` (whether a second pane exists at all, shown or hidden; omitted when there is none — read this226rather than `split`, which is false for a split hidden with ⌘D even though its pane is still alive),227`splitAxis` (`vertical` for left/right or `horizontal` for top/bottom; omitted without a split),228`splitRatio` (the primary-pane divider fraction 0.05-0.95 of the area below the titlebar, of a229session that has a split — shown or hidden; omitted when there's no split, or while the split has never230been shown — a shown split always reports a value, 0.5 when nothing set one) —231the read side of `session resize`, record it to restore the exact divider), `splitFocused`232(which pane holds focus in a session that has a split: `true` = split/right/bottom, `false` = primary/left/top; omitted233when there's no split; the read side of `session focus`, record it to restore focus), and `surfaces`234(`id`, `kind`, `active`, `visible`, and `backedByZmx` on primary/split entries) for `surface zoom` and235`surface cursor`. The tree top level carries `zoomedSurface`236(the control id of the currently zoomed surface, omitted when nothing is zoomed — the read side of237`surface zoom`, so a script can check the zoom state and record-then-restore). It also carries the read238side of the `dashboard` command (all omitted when no dashboard is open): `dashboardMembers` (the pane refs239the open dashboard shows, in grid order — `<session-id>:left` for a primary pane, `<session-id>:right` for240a split pane, so a split session appears as both), `dashboardHighlighted` (the highlighted cell's pane ref —241the one Enter jumps into, focusing that exact pane), `dashboardFontSize` (the absolute font size in points242applied to the cells, omitted when untouched), and `dashboardFontMode` (`auto`|`fixed`|`untouched`).243The top level also carries `pickPending`, the id of the native picker currently awaiting an answer in244that window, omitted when no pick is pending.245246**events**: continuously print control events, subscribing from the current tail when no cursor is247given. Use `--json` for one bare event object per line; filter with repeatable or comma-separated248`--kind status|notify|session.created|session.closed|tree.changed|pane.split|pane.scratch`; resume with paired249`--run RUN --after SEQ`; and set page size with `--limit 1...1000`. The app retains 4,096 events for250one process run. Cursor run changes, expiry, and ahead-of-tail errors are fatal and are never silently251rebaselined. There is no terminal-output event stream.252253**workspace** — `workspace new [name] [--collapsed]` (`--collapsed` creates it closed in the sidebar so you can fill254it with `session new --no-select` without it opening, and keeps it out of the focus set; a plain create255joins the marked set while the filter is applied, so it is visible) · `workspace rename <name>` ·256`workspace delete` · `workspace select` ·257`workspace go --to next|prev` (step the CURRENT workspace one place through the sidebar's visible order, wrapping,258and select the first session of the one it lands on — relative, so no `--target`, and unaffected by259whether a workspace is collapsed; `workspace move` REORDERS instead) ·260`workspace move --to up|down|top|bottom` ·261`workspace focus [on|off|toggle|add]` (mark ONE workspace in the sidebar's focus set — `on` marks it alone and262applies the filter, `off` unmarks it, `toggle` (default) replace-toggles, and `add` marks it alongside263the others WITHOUT switching the filter on; read membership back from the tree workspace node's264`focused` flag) ·265`workspace filter [on|off|toggle]` (apply or suspend that filter for the whole window WITHOUT losing the marked266set — no `--target`; read it back from the tree top-level `workspaceFilter`. Build a working set with267repeated `workspace focus add`, then apply it once with `workspace filter on`; a workspace row renders iff268`sidebarVisible && sidebarMode == "tree" && (!workspaceFilter || focused)` — no workspace row renders at269all with the sidebar hidden or in `flagged` mode, the whole tree renders while the filter is off, and270only while it is on does visibility narrow to the members — and `workspace filter on` with nothing marked is271refused so the pair can never lie) ·272`workspace collapse [--target W] [--window W]` · `workspace expand [--target W] [--window W]` (collapse/expand ONE workspace273in the sidebar tree — the per-workspace pair, distinct from the all-workspace `sidebar expand`/`collapse`;274read the open/closed state back from the tree workspace node's `collapsed` flag, `true` when collapsed and275omitted when expanded).276277**session**278- `session new [--cwd DIR] [--workspace W] [--workspace-name NAME] [--create-workspace] [--command CMD] [--wait] [--name NAME] [--after SID | --before SID] [--no-select]` —279 create (and focus) a session. Target the workspace by id/prefix (`--workspace`) OR by name280 (`--workspace-name`, mutually exclusive); add `--create-workspace` to reuse-or-create the named281 workspace when absent. `--command` runs that program as the session process instead of a login shell282 (argv-only, and with the app's GUI `PATH` — a Homebrew/non-default binary needs an absolute path or a283 `zsh -lc '…'` wrapper, else exit 127; same caveat for `session scratch --command` and `session overlay284 open` below);285 `--wait` (with `--command`, else an error) HOLDS the session open after the command exits, showing the286 press-any-key prompt with the final output intact instead of closing (persists across restart, unlike an287 overlay's live-only wait; read back on `tree`'s `commandWait`);288 `--name` seeds the sidebar label (default: the auto basename). `--after`/`--before` place it directly289 after/before an anchor session (id/prefix/`active`) instead of appending — the anchor carries its own290 workspace, so it's mutually exclusive with `--workspace`/`--workspace-name`. `new --after active` =291 create right after the current session. `--no-select` creates the session in the BACKGROUND — it is292 added to the sidebar but NOT selected or focused, leaving the current selection untouched (the new node293 is not `active` in `tree`); omit it for the default select-and-focus behavior.294- `session duplicate [--target]` — create a fresh session (a plain login shell) in the target's workspace, right295 after it, rooted at the target's focused-pane cwd; selects + focuses it and returns the new id. ONLY the296 directory carries over — no custom name, command, split, scratch, status, flag, font size, or background.297 Equivalent to `session new --cwd <source cwd> --after <source>` in one round-trip, except that a remote298 source's cwd goes through the local rule first (an existing local directory is kept, anything else299 becomes home). Read it back from `tree`: the new node sits directly after its source carrying the300 source's focused-pane cwd (equal to the source node's `tree.cwd` unless the source is a split focused301 off its primary pane, where `tree.cwd` reports the primary, or a remote session, where it can read as302 home).303- `session close [--target T ...]` — close one session, or repeat `--target` to close a batch with one304 grace-period undo.305- `session select` · `session rename <name>` · `session reveal` (select the focused pane's cwd in Finder).306- `session go --to next|prev|first|last|next-attention|prev-attention` — move the selection between sessions.307- `session move <workspace>` (relocate) or `session move --to up|down|top|bottom` (reorder within the308 workspace) or `session move --after SID | --before SID` (place after/before an anchor session; the anchor carries its own309 workspace, so this relocates + positions in one shot, even cross-workspace). For workspace and310 after/before placement, repeat `--target` to move several sessions as one ordered block. Do not repeat311 `--target` with `--to up|down|top|bottom`.312- Shared pane selectors accept `primary`/`left`/`top` for the primary pane and313 `split`/`right`/`bottom` for the split pane. Commands supporting scratch also accept `scratch`.314 Syntax and read-back use canonical `left`/`right`/`scratch`; the invalid-value error keeps those names.315- `session type <text> [--stdin] [--select] [--pane left|right|scratch]` — inject keystrokes (real typing, Enter316 included) into the main pane, the split pane with `--pane right`, or the scratch terminal (even hidden)317 with `--pane scratch`. Pass `--target "$AGTERM_SESSION_ID"` to type into YOUR session, not the user's318 active one (see Addressing). Like `session text`, every `--pane` addresses the surface UNDER a covering319 overlay — by design, so a pane stays drivable whatever is drawn over it — meaning text typed while one is320 open runs in the hidden shell and is invisible until it closes. There is no write twin of321 `session overlay text`: an overlay runs the caller's own program, so nothing types into one. Typing is the322 input a waiting agent asked for, so it clears that pane's `blocked`/`completed` glyph exactly as a323 keystroke does, under Settings ▸ Agent Status ▸ Status reset: on the first key by default, only when the324 text carries a newline under On Enter, never when Disabled; another pane's glyph, an `active` one, and an325 empty payload are left alone.326- `session copy` — print the session's selected text (does NOT touch the system clipboard).327- `session paste` — paste the system clipboard into the session (the socket analogue of ⌘V; read it back with328 `session text`). `--pane left|right|scratch` picks the pane, with the usual role and position aliases;329 omitted is the main pane.330- `session select-all` — select the session's entire terminal buffer (the socket analogue of ⌘A; read the331 selection back with `session copy`).332- `session text [--all] [--lines N] [--pane left|right|scratch] [--pane-id TOKEN]`: print the session buffer333 as plain text. Default is the visible screen of the focused pane; `--pane scratch` reads the scratch334 terminal even while hidden; `--pane-id "$AGTERM_PANE_ID"` follows the same terminal after a role change335 and overrides `--pane` when it resolves; `--all` adds available scrollback (alternate-screen buffers336 have none); `--lines N` keeps the last N lines.337- `session search [needle] [--next|--prev|--close]` — search the terminal scrollback; prints the "N of M" counter.338- `session split [on|off|toggle] [--axis vertical|horizontal]` · `session split close` - second shell, left/right by339 default or top/bottom with `--axis horizontal`. Omitting `--axis` preserves the current axis and the340 legacy left/right behavior. The GUI actions are ⌘D for vertical and ⌘⇧D for horizontal; either341 transposes a shown split of the other orientation. Hide keeps it alive; `close` destroys the pane and342 whatever runs in it.343- `session swap`: exchange the two terminals' physical positions and primary/split roles without restarting344 them. Focus follows the terminal; axis and divider ratio stay fixed. Works on shown or hidden splits and345 under zoom/dashboard; errors when there is no split or either surface is not ready. Read the new primary346 from `tree`'s `cwd`/`title`/`foreground` and the other side from `splitCwd`/`splitForeground`.347- `session scratch [on|off|toggle] [--command CMD]` — full-coverage third shell (hide keeps it alive; `exit`348 recreates). `--command` (when showing) runs a program instead of a shell, run-once like `session new349 --command` (respawns the scratch if one is open). Target your own session with350 `--target "$AGTERM_SESSION_ID"` (see Addressing).351- `session focus [primary|split|left|right|top|bottom|other]` - move focus between split panes. Role and position352 aliases select the same two live terminals; readback remains `left`/`right`.353- `session resize --split-ratio R | --grow-left D | --grow-right D | --grow-primary D | --grow-split D | --grow-top D | --grow-bottom D` - move the split divider (the GUI only drags354 it, or double-clicks it for an even split; bind any other fraction via a355 `command "agtermctl session resize …"` custom action). `--split-ratio` sets356 the absolute primary-pane fraction of the area below the titlebar (left or top; 0..1, clamped to357 0.05..0.95). The grow options are358 aliases for growing the primary or split pane. Prints the applied fraction.359- `session status <idle|active|completed|blocked> [--blink] [--auto-reset] [--sound NAME] [--color #rrggbb] [--shape SHAPE] [--pane left|right|scratch] [--pane-id TOKEN]` — set the sidebar agent glyph (`--sound default` or a system sound name plays a one-shot sound; `--color` tints the glyph for this call only, reverting on the next status set without it; `--shape` (`circle`, `square`, `triangle`, `diamond`, `capsule`, `star`) picks its silhouette for this call only and reverts the same way, read back as the tree `statusShape` field; `--pane` records which pane set it — `left`=main, `right`=split, `scratch` — so foreground typing in another pane won't clear it, and while the session is `blocked` a status from another pane that is not itself `blocked` is refused with `blocked status owned by pane <pane>` so one pane's agent cannot erase the other's request for input, `idle` included since the bundled hooks emit it unprompted; any user-initiated GUI selection (auto-follow, attention-nav ⌃⌥↑/↓, plain session nav, the command palettes, a Dock-menu session, a sidebar row click) reveals that pane when the status needs attention (`blocked`/`completed`); `active` preserves the existing pane selection; the pane reads back as the tree `statusPane` field; the socket `session go next-attention` only steps the selection, it does not itself reveal the pane; `--pane-id` is the hook-forwarded stable surface token (`$AGTERM_PANE_ID`) that resolves the pane's live slot and overrides a stale `--pane` after a promote + re-split — scripts set `--pane` directly and leave `--pane-id` to the hook).360- `session flag [on|off|toggle|clear]` — flag a session for the flagged working-set view (`clear` unflags all).361- `session context <TEXT|--clear> [--target] [--window W]` — set what the session is ABOUT, shown in the362 title bar: a PR number, an issue, the task in hand. Use it when you363 start work a session's name cannot describe. Exactly one of TEXT or `--clear`; a blank TEXT is an error,364 not a second way to clear. Trimmed; max 256 UTF-8 bytes; no control characters (tabs included) or line365 breaks. Persists366 across a relaunch until cleared. Read it back from the tree node's `context` field.367- `session seen [--target] [--window W]` — clear the session's unseen-notification badge WITHOUT changing the368 selection or focus (the focus-free counterpart to `notify`, which raises the badge). Idempotent — a369 no-op when already zero. Read the current count from the tree node's `unseen` field. Use it so an370 orchestrator can acknowledge a driven session's notifications without pulling focus to it.371- `session restore ("cmd" | --none | --clear) [--pane left|right] [--pane-id TOKEN]` — pin what a pane re-runs on372 the NEXT launch, overriding the captured foreground command. A `"cmd"` shell line pins it, `--none` pins373 nothing (a plain shell), `--clear` drops the override back to auto-capture. Written now, consumed on the374 next launch (it never touches the running session), and STICKY — fires again on every restart until375 cleared. It runs in `rerun` mode. In fresh-shell or live mode, a command or `--none` still saves policy376 for a future rerun launch and returns a note naming the active mode; `--clear` works in every mode. A pin377 never opts one session out of live mode. Deliberate pins bypass `restore-denylist.conf`. Read back as the tree node's378 `restoreCommand`/`splitRestoreCommand`. `--pane right` needs a split; `scratch` is rejected. `--pane-id`379 (the shell's `$AGTERM_PANE_ID`) resolves the pane's live slot — unlike `session status`, a token that380 does not resolve errors unless `--pane` is also given. For a non-idempotent command like381 `claude --resume … --fork-session` (which mints a new session on every restart), a Claude Code382 `SessionStart` hook rewrites the override to the live id on every start so the next restart reattaches383 instead of forking. The pinned value is shell code stored in the state file and readable via `tree`, so384 it must not carry secrets. See examples.md.385- `session background image <path> [--opacity F] [--fit contain|cover|stretch|none] [--position P] [--repeat]` ·386 `session background text <text> [--color #rrggbb] [--opacity F] [--fit ...] [--position ...]` ·387 `session background color <#rrggbb>` · `session background clear` — composite an image (PNG/JPEG) or rasterized text388 behind the terminal as a watermark (auto-fitting the window, re-fits on resize), or set a solid389 terminal background color. Per session; survives restart. `--opacity` 0.0–1.0. (An image/text watermark390 renders the pane opaque, overriding window translucency, so it shows; a `color` takes no opacity and391 honors the Settings window translucency instead.)392- `session overlay open <command> [--cwd DIR] [--wait] [--block] [--size-percent N] [--background-color #rrggbb] [--follow] [--pane left|right]` ·393 `session overlay resize (--size-percent N | --full)` ·394 `session overlay close [--pane left|right]` ·395 `session overlay result [--pane left|right]` ·396 `session overlay copy [--pane left|right]` ·397 `session overlay text [--all] [--lines N] [--pane left|right]` — run a program on top of a session; `--block`398 waits and exits with its status.399 `session overlay copy` returns the selection made INSIDE the overlay and `session overlay text` its terminal buffer:400 `session copy` and `session text` both address the pane the overlay COVERS, so a selection made in the401 overlay reads there as `no selection` and `session text --pane right` returns the shell underneath.402 Reach for them when the read is NOT chord-driven — polling from outside, or reading some time after the403 fact. A chord already gets the firing surface's selection synchronously in `$AGT_SELECTION`, the404 overlay's included, so a custom command should use that rather than a later socket read.405 `session overlay text` returns a TUI's drawn screen wrapped as rendered — for a program's OUTPUT, still prefer406 its own output file.407 `session overlay resize` changes an ALREADY-OPEN overlay: `--size-percent N` (1-100) makes it a floating panel,408 `--full` switches it back to the full-pane overlay; the program keeps running (no re-spawn).409 `--pane left|right` scopes the overlay to ONE split pane instead of the whole session, leaving the410 sibling pane live and interactive; left and right are independent and may both be open at once. A pane411 overlay is ALWAYS full-pane, so `--pane` cannot combine with `--size-percent` and `session overlay resize`412 takes no `--pane`. Everything else is identical to the session-wide overlay. A non-split session413 accepts `--pane left`. `AGTERM_PANE` is only the shell's spawn role and may be stale after promotion or414 `session swap`, so a long-running shell must not assume `--pane "$AGTERM_PANE"` still names its slot.415 A pane that is not currently rendered is refused with `pane not visible`. A SHOWN416 split renders both panes, a HIDDEN one renders only the FOCUSED pane, so the refused one is the pane417 that does not have focus.418 Target with `--target "$AGTERM_SESSION_ID"` for YOUR session (default `active` is the user's selection).419 **By default `session overlay open` does NOT switch the user** — full and floating (`--size-percent N`, 1-100)420 both open on `--target` and run their program in the background; the panel appears when the user visits421 that session. **Pass `--follow` to select the target after opening** (a no-op if it is already active):422 use `--follow` when you want the user pulled to the overlay, omit it to open quietly on your own or423 another session.424 `--background-color` gives the overlay pane its own solid color, independent of the session's. An425 overlay is a real terminal (pty), which is also how you **display an image inline** — via the bundled426 `scripts/show-image.sh` (see below).427- `session hud [open] <message> [--detail T] [--spinner] [--spinner-style S] [--position P] [--background-color #rrggbb] [--text-color #rrggbb] [--size-percent N] [--pane P] [--pane-id ID]` ·428 `session hud update <message> [--detail T] [--spinner] [--spinner-style S] [--position P] [--text-color #rrggbb] [--size-percent N] [--pane P] [--pane-id ID]` ·429 `session hud close` — post a small **passive** panel over the session saying what you are doing430 ("gathering options…"). Unlike an overlay it takes no input and steals nothing: the session keeps first431 responder, the user keeps typing, and the terminal behind it is neither dimmed nor click-blocked. Use it432 for the seconds an agent needs before it can show something (computing picker items, waiting on a slow433 command), then take it down. `open` is the default subcommand, so `session hud "…"` posts; a message that is434 literally `update` or `close` needs the explicit `session hud open` verb. `--detail` adds a dim second line,435 `--spinner` animates a glyph in the default `bar` style and `--spinner-style bar|braille|circle|blocks|dot`436 picks another, turning the spinner on by itself (`dot` blinks instead of animating, for a panel up for437 minutes; an update may switch style in place). `--spinner-style none` is accepted and leaves the panel438 static, so the `none` a read-back reports round-trips. `--position` anchors it to any of the nine439 `top-left|top-center|top-right|center-left|center|center-right|bottom-left|bottom-center|bottom-right`440 (default `center`), the same set `session background` takes; every anchor off center holds a fixed margin441 off that pane edge automatically, so a corner keeps the panel out of the text the user is reading. The442 bare `top`/`bottom` are still accepted for `top-center`/`bottom-center`, and the read-back reports the443 canonical anchor. `--pane primary|left|top|split|right|bottom` makes that pane the coordinate space for444 the anchor, size cap, and margin. `--pane-id "$AGTERM_PANE_ID"` follows the same shell after pane swaps or445 promotion and overrides `--pane` when it resolves. An unknown token needs a `--pane` fallback. Open refuses446 a pane that is not visible. Hiding a target keeps the HUD alive until the pane returns; closing it closes447 the HUD. The panel is sized from the message on both axes:448 width from the longest line, height from the number of them — so a title and a subtitle give a wide, short449 panel, not a square one. `--size-percent N` (1-100) overrides the WIDTH only, bounded to 10-80% of the450 pane, since a message must never cover the session it is about, so a requested 100 reads back as 80. The451 height always follows the message. `--text-color` colors the panel's TEXT and `--background-color` its452 backing, independently. `session hud update` repaints in place with no re-spawn and no blink,453 and REPLACES the whole spec. Repeat `--detail`/`--spinner`/`--text-color`/`--pane`/`--pane-id` to keep them, since an omitted454 one drops. It takes no `--background-color`: the s455456…(truncated)