| name | agterm |
| description | 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 and the agterm-scoped ghostty config; subscribe to status, notification, lifecycle 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 request.
|
| when_to_use | Trigger on: agterm, agtermctl, AGTERM_SESSION_ID, and, from inside a session, plain requests such as split the pane, close the overlay, show a message over the session, show a question dialog, agtermctl ask, show an image inline, search the scrollback, attach a session from another Mac, what recipes are there, the keymap editor will not open.
|
| allowed-tools | Bash(agtermctl *) |
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, and
structural tree changes. 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.
The quick terminal is scratch (not in the tree) and belongs to no window, so 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)\"'"
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),
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; 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).