Documentation

agterm

A native macOS terminal for working with AI coding agents across many sessions at once.

Overview

agterm is intentionally opinionated: rather than scattering shells across tabs, it organizes them into named workspaces, each holding the sessions for one project or context. Several agent-driven sessions run side by side and you move between them without losing track of which is which.

The design is deliberately minimal: a sensible minimum out of the box, plus a complete control API and CLI on top. Almost everything is scriptable, so anything past the defaults you build yourself. None of it is limited to agents — it works as a capable general-purpose terminal too.

For the real terminal work — rendering, VT parsing, and shell I/O — agterm embeds Ghostty's engine (libghostty); everything above is agterm's own.

Workspace organization

A vertical, two-level sidebar groups sessions under named workspaces.

Programmatic control

agtermctl drives almost everything over a local socket.

Splits, scratch & overlays

Split a session into two shells, drop a scratch over it, or run a program in an overlay.

Agent skill

An installable skill teaches Claude Code or Codex to drive agterm through agtermctl.

Status at a glance

Tinted glyphs show which of many agents needs you.

Install

Available now — Signed, notarized builds are published on the releases page. Install via Homebrew or the direct download below; building from source is optional.

Releases are signed and notarized for Apple Silicon (arm64) Macs running macOS 14 or later, so they open without any Gatekeeper workaround.

Homebrew

$ brew install --cask umputun/apps/agterm

The cask also installs the agtermctl command-line tool, so cask users shouldn't run the in-app installer as well.

Direct download

Download the latest .dmg from the releases page, open it, and drag agterm.app into /Applications.

Optional Help-menu installers

None are needed to use agterm as a terminal; each connects it to a wider workflow, and you can run any of them later. The first launch on a machine points them out in a welcome dialog, which offers the skill and the status hooks and never appears again.

  • Install Command Line Tool… puts the bundled agtermctl on your PATH (a symlink in /usr/local/bin) so you can script the app. The Homebrew cask already installs it.
  • Install Agent Status Hooks… lets a coding agent (Claude Code, Codex, Pi, OpenCode, or others) report its state onto its session's row, so you can tell which of several running agents is active, blocked, or finished.
  • Install Agent Skill… teaches Claude Code or Codex how to drive agterm through agtermctl, so an agent inside a session can build its own layout, run overlays, and manage windows without you explaining the API. It drives the app through the CLI, so install that one too. The same skill is also published as a plugin from the repository — claude plugin marketplace add umputun/agterm then claude plugin install agterm@agterm, or the matching codex plugin commands. Use one route or the other, not both.

Build from source

Requires macOS 14+, Xcode 26 with xcodegen on PATH (plus its Metal Toolchain), and Homebrew.

# build libghostty + stage resources (idempotent)
$ scripts/setup.sh
# generate the Xcode project, build Debug, launch
$ scripts/run.sh
# host-free unit tests, no Xcode needed
$ cd agtermCore && swift test

Workspaces & sessions

A session is one running shell with a name, a working directory, and its own scrollback — the unit you work in and the row you see in the sidebar. A workspace is a named group of sessions for one project or context. Sessions can move between workspaces while still running, keeping their shell and scrollback.

  • Two-level sidebar tree: workspaces, each containing sessions. Each row carries a leading kind icon — a filled folder for a workspace, an outlined terminal for a session.
  • Default session name is the basename of its working directory. Renaming pins a custom name; clearing it reverts.
  • Add workspaces and sessions, and toggle the workspace filter and the flagged view, from the four-icon bar at the bottom of the sidebar. Rename inline (double-click a row). Copy Name on a session or workspace row copies its name to the clipboard. Duplicate Session from a single session's context menu for a fresh shell in the same workspace, right after it, in its current directory — only the directory carries over. Shift-click and ⌘-click select multiple sessions for batch flag, close, move, and drag. Dragged sessions keep running, shell and scrollback intact.
  • Drag up to 20 folders from Finder onto the tree to open sessions there. Collapsed workspaces spring open while hovering and close again on cancel. Reveal a session's focused-pane directory in Finder from its context menu.
  • Narrow a crowded sidebar two ways: flag sessions across workspaces into a flat working-set view (a flag is durable and survives a move), or focus a set of workspaces to collapse the tree to just their sessions. Focus one workspace to zoom to it, or mark several from their row menus and apply the filter once with the sidebar's grid button — marking never narrows the tree on its own, and suspending the filter keeps the set for one click back. If narrowing the sidebar would hide the session you are working in, agterm moves you to the most recent session still on screen, unless the narrowing leaves nothing visible at all. The two are independent.
Session context menu
A session's right-click context menu.

Terminals: split, scratch, quick, search, overlay

Split panes ⌘D

A session can split into two shells side by side. Both panes share one sidebar row — a split is one session with two terminals, not two sessions. One pane is focused at a time and the divider position is remembered — drag the divider to resize the panes, double-click it to snap back to an even split.

Quick terminal ⌃`

A single throwaway shell per window, not tied to any session, overlaid at 90% of the window and opening in the active session's directory. Click again or the surrounding margin to dismiss; hiding keeps its shell alive.

Per-session scratch ⌘J

An extra shell belonging to one session, for a quick aside next to your main work. It covers the session full-screen and hides again without killing it. While it is up, ⌘D and the split button hide it rather than rearrange the panes beneath. Opens in the session's directory; not restored across launches.

In-terminal search ⌘F

A search bar at the top of the focused terminal highlights matches in live scrollback with an "N of M" counter. Enter steps forward, Shift-Enter back, Esc closes.

Overlay control API

Runs one program in a temporary terminal over a session and disappears when the program exits, leaving the session unchanged. Mostly driven from the control API to launch an interactive program (a diff viewer, a process monitor) — full-size, as a floating panel, or scoped to one split pane while the sibling pane stays live. The same slot also holds a HUD, a small passive panel carrying a message instead of a program, which leaves the session focused and typable underneath. See the agtermctl reference.

Terminal zoom ⇧⌘↩

Fills the whole window with one terminal surface — a pane, the scratch, an overlay, or the quick terminal — hiding the sidebar and collapsing the title bar to a slim strip with the traffic lights, the window title, and an exit button (⌘W leaves zoom too). A view mode, not a layout change: exiting restores split ratios, focus, and visibility exactly as they were, and everything else keeps running behind it. Scripts can zoom any surface by id with agtermctl surface zoom. Distinct from macOS window zoom and full screen, which size the window itself.

Dashboard control API

Watch several agents or builds at once: sessions' live output in one grid overlaid on the window. The cell unit is a session+pane — a non-split session is one cell, a split session is two (its left and right panes), capped at nine cells. Each cell's name chip also reflects the session's agent status, filling with the status color and pulsing while it blinks, unless macOS Reduce Motion is enabled; the status color and text remain visible without the repeating animation. It is view-only — no cell's terminal takes input; the arrow keys move a highlight, Enter (or a single mouse click on a cell) jumps into that session and focuses that exact pane, and Esc closes. Opened over the control channel with agtermctl dashboard <ids…>, or agtermctl dashboard --mru to fill it from the window's most-recently-used sessions instead of naming ids. An id may carry a :left/:right suffix to place one pane instead of the whole session — the same form tree --json reports in dashboardMembers — so agtermctl dashboard "$a:left" "$b:right" keeps the panes you did not ask for out of the nine-cell budget. The most-recently-used grid also has a built-in opener — ⌘⇧D (or Navigate ▸ Dashboard, the command palette's Dashboard, or the title-bar grid button) opens it auto-sized. Cell fonts size absolutely (--font-size) or scale to the grid (--auto-size). Mutually exclusive with terminal zoom.

The dashboard grid showing several live sessions at once
The dashboard: several sessions' live output in one view-only grid.

Windows

A window is a top-level bundle of workspaces and sessions in its own on-screen macOS window, with its own sidebar and its own sessions — so "work" and "personal" can run as two separate windows at once, each with its own tree. Keep a library of windows, open one per screen, and create, rename, or delete them from the File menu (New Window ⌥⌘N) or the action palette. The set of windows open at quit reopens on the next launch, frames restored. Windows are also fully scriptable — agtermctl window can create, raise, move, resize, and minimize them, so a few lines of shell can give every window the same frame and park all but the one you are on, turning several windows into what feels like one that switches contents.

Notifications

A program in any session can raise a desktop notification (via OSC 9 / 777, or agtermctl notify). It surfaces as a macOS banner and an unseen-count badge on the sidebar row (rolled up onto a collapsed workspace); clicking the banner brings agterm forward and focuses the exact pane that raised it, and focusing a session clears its badge — or clear it headlessly with agtermctl session seen so an orchestrator driving a session over the socket can acknowledge it without pulling focus (agtermctl tree --json reports each session's unseen count). Banners and count badges toggle independently in the Notifications settings, which also sets the Dock-icon bounce for a background notification (off, once, or until you focus agterm) and an optional notification sound (a system sound attached to each delivered banner; None by default, silenced by Do Not Disturb). For a coding agent that just needs to say it is waiting, agent status is usually the better fit.

Accessibility

Voice dictation tools that probe for a focused text field — the system Dictation, MacWhisper, and similar assistive apps — engage over the terminal: the on-screen pane advertises itself to the accessibility system as an editable text area, so a hold-to-dictate widget anchors to it and dictated text lands at the prompt. Text arrives at the cursor the same way typing does, and an insert carrying a newline, a tab, or any other control character goes in as a bracketed paste, so a program that accepts bracketed paste takes it as literal text instead of running the line or completing the word. That last part is the same caveat ⌘V carries: at a raw prompt with bracketed paste off, a trailing newline still submits and a tab still triggers completion.

Two limits are worth knowing. The scrollback is deliberately not mirrored, so the exposed pane reads to a screen reader as an empty text area named "Terminal" rather than as the terminal's contents — mirroring the grid is a separate, much larger piece of work. And because the terminal appends at the cursor with nothing to read back, a dictation tool that re-sends its whole transcription on every revision (rather than only the new words) will concatenate its drafts at the prompt; tools that insert incrementally, MacWhisper among them, are unaffected.

Customization & settings

A live-preview theme picker (View ▸ Select Theme…, or the action palette) applies each of the 512 bundled themes to the open terminals as you move through the list — Enter commits and syncs it to Settings, Esc reverts to the one you started on. Settings (⌘,) has six tabs, and changes apply live:

General — mouse scroll speed, right-click-to-paste, whether clicking a workspace row expands or collapses it (on by default; the disclosure triangle always does), where a new session opens, an opt-in re-run of each pane's foreground command on restart, an opt-in confirm before closing a session, and whether to load your global Ghostty config.
Appearance — terminal font and theme, the toolbar mode (Normal with the working directory, Compact, or Hidden for a full-bleed terminal with no titlebar or traffic lights), window background opacity and blur, the sidebar tint, the sidebar font size, the palette and switcher font size, and how much the terminal behind a floating panel dims — the inactive half of a split, and the session left visible around a floating overlay or the quick terminal. With macOS Reduce Transparency enabled, agterm temporarily presents translucent windows, command palettes, and session switchers as opaque and unblurred without changing the saved opacity or blur; disabling it restores those settings. Reduce Motion keeps status colors and glyphs visible but suppresses their repeating sidebar and dashboard pulses.
Interface — turn individual chrome controls on or off (each shown by default): in the title bar the sidebar toggle, the session name, the window name, and the recent-sessions, scratch, split, dashboard, and quick-terminal buttons; in the sidebar the new-workspace, new-session, flagged-view, and workspace-filter footer buttons plus the per-workspace add-session + revealed on hover. A Multiple Windows option (off by default) shows the sidebar only in the frontmost window and collapses it on every other, so switching windows moves the sidebar with focus.
Notifications — the banner, the unseen-count badge, the Dock-icon bounce for a background notification (off / once / until focused), the notification sound (None by default), and the title-bar attention indicator.
Agent Status — the status-glyph colors and shapes, the blocked-session sound, and an idle timeout to auto-follow blocked sessions.
Key Mapping — the directory holding keymap.conf, a list of any parse errors, and a reload button.
Keymap editor
The keymap editor.

agtermctl reference

Looking for the full list? All 74 commands, with every argument, return value, and error, live on the Command reference →

agterm can be driven from a script over a local unix-domain socket through the companion CLI, agtermctl. This is for fire-and-forget scripting that manages workspaces and sessions, injects text, invokes control actions, and polls a control-event feed for status and lifecycle changes. There is no terminal-output streaming.

To open a terminal at a directory without the CLI, open -a agterm <path> — or right-click a folder in Finder and choose Open With ▸ agterm. agterm adds a session in that directory to the last-active window. This works when agterm is already running (its usual state); if it isn't, launch agterm first, then run the command. The socket equivalent, and the way to place the session precisely, is agtermctl session new --cwd <path>.

Each command targets a session or workspace by its UUID, a unique prefix of that UUID (git-style), or the keyword active (the selected session / current workspace). --target defaults to active, so the current one rarely needs naming. --target/--workspace take an id, a prefix, or active — never a name. For workspaces, active is the one a new session lands in: a workspace you just created in the foreground, otherwise the selected session's, otherwise the last one. A newly created workspace stays the target until the selection changes — to a different session or to none — or until you delete it or hide it behind the workspace filter. workspace select moves the target to the workspace you name, an empty one included. So New Session (and session new) right after creating one lands in the new workspace rather than the one you came from. Mutating commands normally print the affected id; batch session close and session move accept repeated --target options and print the number of sessions actually changed. Add --json for the raw response, or --socket PATH to override the socket. The exit code is zero on success, non-zero on error.

Native picker

agtermctl pick reads nonblank lines or a JSON array of {id,label,subtitle?} objects from stdin and opens agterm's fuzzy picker. It blocks by default and prints the picked, custom, or cancelled result as bare JSON. Add --no-block to get the picker id immediately, then use pick result or pick cancel. The target tree's top-level pickPending field carries that id while the picker waits.

Typing matches item labels only; a subtitle is shown but never searched, so consequence text on one row cannot filter out its safer neighbour. An empty query lists the items in the order they were supplied, so the caller's first item is the one Return runs on open. --query TEXT prefills the field and filters immediately, which ranks by match score and therefore does not preserve that order. With --allow-custom the item list may be empty, which turns the picker into a plain text prompt: the custom row appears as soon as the query is nonblank, whether prefilled by --query or typed. An itemless call still reads stdin, so redirect it (< /dev/null) or it blocks.

printf '%s\n' staging production | agtermctl pick --prompt "Deploy where?"

agtermctl pick --allow-custom --query "$name" --prompt "Rename to" < /dev/null

id=$(printf '%s\n' alpha beta | agtermctl pick --no-block | jq -r '.id')
agtermctl pick result "$id"
agtermctl pick cancel "$id"
agtermctl tree --json | jq -r '.result.tree.pickPending // empty'

Sessions & workspaces

# print the workspace/session tree with ids
agtermctl tree

# create a workspace, capture its id, open a session in it
ws=$(agtermctl workspace new work)
agtermctl session new --workspace "$ws" --cwd ~/src/agterm

# run a command as the session's process (argv-style; wrap in sh -c for shell syntax)
agtermctl session new --command "ssh user@host"
agtermctl session new --command "zsh -lc 'make test'" --wait   # hold open after the command exits (press any key to close); needs --command
agtermctl session new --name myhost --workspace-name servers --create-workspace

# step / reorder / relocate
agtermctl session go --to next    # next|prev|first|last
agtermctl session move --to up    # reorder: up|down|top|bottom
agtermctl session move "$ws"     # relocate to a workspace
agtermctl session new --after active   # create right after the current session (--before to precede)
agtermctl session new --cwd ~/src/agterm --no-select   # create in the background without switching to it
agtermctl session duplicate --target 9f3c   # a fresh shell in that session's workspace and cwd, right after it
agtermctl session move --after 9f3c   # place after an anchor (its workspace is used; relocates cross-workspace)
agtermctl session move "$ws" --target 9f3c --target abcd   # move a batch as one ordered block
agtermctl session close --target 9f3c --target abcd   # one grace-period undo for the batch
agtermctl workspace move --to top    # reorder a workspace
agtermctl workspace new work --collapsed    # create a workspace closed in the sidebar (fill with session new --no-select)
agtermctl workspace collapse --target 9f3c  # collapse one workspace; workspace expand re-opens it
agtermctl workspace focus on    # mark this workspace alone and apply the filter (on|off|toggle|add)
agtermctl workspace focus add --target a1b2   # mark another one without narrowing the tree yet
agtermctl workspace filter on      # apply the marked set; filter off suspends it without losing the set
agtermctl session reveal --target 9f3c  # reveal the focused pane's cwd in Finder

Typing, selection & text

# inject text (every newline is a real Return; use $'…\n' or --stdin)
agtermctl session type --target 9f3c $'make test\n'
echo 'make test' | agtermctl session type --stdin

# route to a pane: left (default) | right (split) | scratch (even when hidden)
agtermctl session type --pane right $'ls\n'

# type into a background session without changing focus
agtermctl session type --target "$id" $'echo hi\n'

# read a pane's selection (does not touch the clipboard) or its output
sel=$(agtermctl session copy --target 9f3c)
agtermctl session text --pane scratch --target 9f3c

# paste the system clipboard (⌘V), or select the whole buffer (⌘A) then copy it
agtermctl session paste --target 9f3c
agtermctl session select-all --target 9f3c

Splits, scratch, quick & search

agtermctl session split toggle
agtermctl session resize --split-ratio 0.7  # or --grow-left/--grow-right D
agtermctl session scratch toggle     # on|off|toggle
agtermctl session flag on        # on|off|toggle|clear
agtermctl session seen --target 9f3c  # clear the unseen badge, focus-free
agtermctl sidebar mode flagged    # tree|flagged|toggle
agtermctl quick toggle        # show|hide|toggle
agtermctl quick type 'ls -la'  # or --stdin; quick text reads it back
agtermctl surface zoom        # fill the window with the active surface (show|hide|toggle; --target surface:<id>:right)
agtermctl dashboard "$a" "$b" "$c" --auto-size  # view-only grid; a split session is two cells, capped at 9 panes (--mru; --font-size N | --auto-size; --close)
agtermctl font inc           # main pane font size
agtermctl font dec --pane right  # just the split pane (left|right|scratch)

# open the search bar, print the "N of M" counter
agtermctl session search "error"
agtermctl session search --next     # --prev | --close

Overlays

# full overlay on a session (hides the shell beneath)
agtermctl session overlay open "revdiff HEAD~3" --target 9f3c

# floating framed panel at 70% of the pane, optionally tinted
agtermctl session overlay open "htop" --size-percent 70
agtermctl session overlay open "revdiff HEAD~3" --size-percent 80 --background-color "#2a1a3a"

# switch the user to the target as the overlay opens
agtermctl session overlay open "revdiff HEAD~3" --size-percent 80 --target 9f3c --follow

# keep it open after exit, or block until it exits and inherit its status
agtermctl session overlay open "make test" --wait
agtermctl session overlay open "make test" --block

# resize an open overlay in place — floating percent, or back to full (the program keeps running)
agtermctl session overlay resize --size-percent 60 --target 9f3c
agtermctl session overlay resize --full --target 9f3c

agtermctl session overlay close --target 9f3c
agtermctl session overlay result   # last overlay's exit status

# cover only one split pane, leaving the sibling pane live
agtermctl session overlay open "revdiff HEAD~3" --target 9f3c --pane right

--wait keeps a "press any key to close" prompt so you can read the final output; --block reports only the exit status (the overlay never captures stdout — a TUI writes its own result file). By default an overlay opens on its target without switching the active session (full and --size-percent floating alike); --follow switches the user to the target. session overlay resize changes an open overlay in place — --size-percent makes it floating, --full switches it back — without restarting the program. A (overlay) tag in agtermctl tree marks a session whose overlay is open.

--pane left|right scopes the overlay to one split pane instead of the whole session: it covers exactly that pane and leaves the sibling pane visible and interactive, so an agent working in the split can show a diff or a TUI over its own pane without blanking the one you are reading. The two panes are independent and may both hold an overlay at once, each with its own background color. A pane overlay is always full-pane — there is no floating variant, so --pane cannot combine with --size-percent and session overlay resize takes no --pane — and everything else matches the session-wide overlay. A non-split session accepts --pane left, since its shell reports AGTERM_PANE=left, so a script can pass --pane "$AGTERM_PANE" without checking whether the session is split. A pane that is not on screen is refused with pane not visible; hiding the split after opening is fine, the program keeps running and reappears when the split comes back. close and result take the same --pane, ⌘W dismisses the focused pane's overlay before it would close the session (one on the other pane is not in front of you, so ⌘W keeps its ordinary meaning), and agtermctl tree --json reports the covered panes as paneOverlays.

Message panel (HUD)

# post a message over a session; the session keeps focus and stays typable
agtermctl session hud "gathering options…" --spinner --detail "scanning branches" --target "$AGTERM_SESSION_ID"

# repaint in place, no re-spawn and no blink
agtermctl session hud update "ready" --detail "pick a branch" --target "$AGTERM_SESSION_ID"

# anchor it and set its width by hand instead of measuring the message
agtermctl session hud "deploying" --position top-right --size-percent 30

# color both halves of the panel
agtermctl session hud "deploying" --text-color "#7ec07e" --background-color "#202020"

agtermctl session hud close --target "$AGTERM_SESSION_ID"

A HUD carries a message rather than a program, and it is passive: the session keeps first responder and stays typable under the panel, the terminal behind it is neither dimmed nor click-blocked, and nothing waits for an answer. It is for the seconds a script or an agent needs before it can show anything, so the session says what is happening rather than sitting silent. open is the default subcommand, so session hud "…" posts one. --position anchors it to any of the nine positions session background takes — top-left, top-center, top-right, center-left, center, center-right, bottom-left, bottom-center, bottom-right — defaulting to center, with every anchor off center holding a margin off that pane edge on its own; a corner is what keeps a panel out of the text you are reading. The bare top and bottom this took before are still accepted for the middle column. The panel comes 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 rather than a square one. --size-percent overrides the width only, bounded to at most 80% of the pane, so a message never covers the session it is about. --text-color sets the text's own color and --background-color the panel's backing, so the two halves are independent. An update replaces the whole message, so --detail, --spinner and --text-color have to be repeated to survive it. The spinner takes a style — --spinner-style bar|braille|circle|blocks|dot|none, which turns it on by itself, with dot blinking rather than animating — and an update may switch style in place; --background-color is the exception, read once when the panel is created and kept — in the panel and in the read-back — across every update, which is why an update does not take it at all.

It shares the overlay slot, so a second hud replaces the first, session overlay open replaces a HUD, and ⌘W or session overlay close takes one down; a HUD over a running program is refused instead, because a message is replaceable and a program is not. session overlay result over a HUD errors, and session overlay resize accepts a percent but refuses --full, which would cover the session the message is about. agtermctl tree --json reports the panel as the session node's hud object, with overlay reading false beside it; there is no event, so a script that needs the state polls the tree.

Windows

agtermctl window list     # id name [open] [active]
w=$(agtermctl window new work)
agtermctl window new proj-b --minimized  # create one, parked in the Dock
agtermctl window select "$w"  # raise it (opening if closed)
agtermctl window rename "$w" personal
agtermctl window minimize "$w" on  # park it in the Dock (off restores)
agtermctl window close "$w"   # close its window (bundle kept)
agtermctl window delete "$w"  # delete (last window can't be deleted)

# --window targets a specific window's tree on session/workspace/tree/font
agtermctl tree --window "$w"
agtermctl session new --window "$w" --cwd ~/src/agterm

Keymap, config & notify

agtermctl keymap reload  # re-read keymap.conf, returns the diagnostic count
agtermctl config reload  # re-read the ghostty config, returns the diagnostic count
agtermctl notify --title Build --body "tests passed"
In-session env — a shell inside agterm gets AGTERM_ENABLED, AGTERM_WINDOW_ID, AGTERM_WORKSPACE_ID, AGTERM_SESSION_ID, AGTERM_SOCKET (the live socket path), and AGTERM_PANE (which pane the shell runs in — left (main), right (split), or scratch; unset in an overlay), and AGTERM_PANE_ID (a stable per-surface token the agent-status hook forwards as session status --pane-id, so a promoted-then-re-split pane still tags correctly), so a script can drive its own window without hard-coding ids.

The cookbook collects complete agtermctl workflows, each in its own directory with a README and, where it needs one, its scripts: switching the sidebar to a single project, closing a project's workspaces and bringing them back later, picking a path with fzf and typing it into the shell, and giving each tab its own Claude Code or Codex conversation across a restart. They are written to be copied into your own setup and edited, not only read; cookbook/CONTRIBUTING.md has the rules for adding one.

Recipes come from other people as well as the maintainer. Every one is reviewed before it is accepted, but they are shell scripts you run on your own machine against your own sessions, and several close sessions or delete workspaces, so read a recipe before you run it.

Customizing keys

agterm reads a user-editable, kitty-flavored keymap file at ~/.config/agterm/keymap.conf. It rebinds built-in menu shortcuts and defines custom shell commands bound to keys (and listed in the action palette). The file is optional — a commented starter is written on first launch, and the directory can be changed in Settings ▸ Key Mapping. Two verbs; blank lines and # comments are ignored.

# rebind a built-in to a single chord (no leader sequences for built-ins)
map cmd+shift+l   toggle_split
map ctrl+shift+k  command_palette

# define custom commands ("name" shows in the palette; chord optional)
command "Open in Zed"  cmd+shift+e  open -a Zed {AGT_SESSION_PWD}
command "Lazygit"      ctrl+a>g     agtermctl session overlay open 'zsh -lc lazygit' --socket {AGT_SOCKET}
command "Deploy"               ./deploy.sh

A chord is modifier words (ctrl, cmd, opt, shift) joined by + and a base key (a single character or tab/space/return/delete/left/right/up/down). A Shift-typed symbol is written shift+<base> (e.g. shift+/ for ?, shift+= for +) — the base key, not the shifted symbol. Custom commands may also use a leader sequence (ctrl+a>g), and their chord must include a modifier — a bare key can't shadow a plain terminal key.

Chords are written in Latin and keep working on a non-Latin keyboard layout. A layout that cannot type ASCII — Russian, Greek, Hebrew, Arabic, Thai — resolves every chord by the physical key position, so cmd+o still fires on the key marked O even though it types щ. A layout that can type ASCII binds what it types, so an alternative Latin layout keeps its own letter positions: on Dvorak, cmd+o follows the O you actually type.

Bindable built-in actions

new_window   rename_window   delete_window
new_workspace   rename_workspace   delete_workspace
new_session   open_directory   rename_session   duplicate_session
close_session   reopen_recent   undo_close   clear_status
increase_font_size   decrease_font_size   reset_font_size
toggle_split   toggle_scratch   toggle_search
toggle_sidebar   toggle_flag   toggle_flagged_view
focus_left_pane   focus_right_pane   focus_workspace   toggle_workspace_filter
previous_session   next_session   first_session   last_session
previous_attention_session   next_attention_session
quick_terminal   session_palette   command_palette
custom_command_palette   show_attention
select_theme   toggle_fullscreen   toggle_terminal_zoom
dashboard

Command tokens

{AGT_SESSION_ID}   {AGT_SESSION_NAME}   {AGT_SESSION_PWD}
{AGT_WORKSPACE_ID}   {AGT_WORKSPACE_NAME}
{AGT_WINDOW_ID}   {AGT_WINDOW_NAME}
{AGT_PANE}   {AGT_SELECTION}   {AGT_SOCKET}

Tokens expand at fire time (also exported as $AGT_* env vars on the spawned process). A token is substituted raw into the shell line, so for content you don't control — {AGT_SELECTION}, and also {AGT_SESSION_NAME}/{AGT_SESSION_PWD} (a remote host can set these via OSC) — prefer the matching quoted env var, e.g. "$AGT_SELECTION".

Detached, no TTY — a custom command runs as a detached /bin/sh -c with no controlling terminal, so it suits fire-and-forget launches (GUI apps, scripts) — not interactive TUIs. Run a TUI like lazygit in an overlay (agtermctl session overlay open) or a scratch terminal, which have a real TTY. A non-zero exit posts a notification banner.
GUI PATH — a custom command resolves its binaries against the app's GUI PATH: the launchd default plus the bundled agtermctl, /usr/local/bin and /opt/homebrew/bin. A bare agtermctl or Homebrew binary works; anything else your shell profile adds does not, and fails with exit 127. Give it an absolute path or wrap the line in zsh -lc '…'zsh -ilc '…' when that PATH comes from ~/.zshrc, which -lc does not read. The program an overlay or scratch terminal runs gets the app's own unwidened PATH and always needs one of those.

Open the file with File ▸ Edit Keymap… (or the ⌃⇧P palette): it opens in a 95% overlay running $VISUAL/$EDITOR (falling back to vi) and reloads on quit. Apply edits made elsewhere with File ▸ Reload Keymap or agtermctl keymap reload. A malformed line never discards the rest — it surfaces in the diagnostics list in Settings ▸ Key Mapping while the good lines still apply.

To check what is actually bound, agtermctl keymap list prints every built-in with the chord it resolved to, the custom commands, each diagnostic in full, and the key equivalents the menu bar is really carrying. If a binding will not fire, compare the last two: an action whose chord no menu item holds is usually a menu problem, not a keymap one. The one deliberate exception is undo_close (⌘Z), delivered by a key monitor rather than a menu item, so it never appears in the menu list.

v1 limitations

  • Built-in rebinds are single-chord only; leader sequences work only for custom commands.
  • A map line can't bind a bare, modifier-less arrow — a built-in rides an always-on menu key equivalent, so a bare arrow would swallow the key in the terminal, the palettes, the dashboard grid, and every text field; any modifier makes it bindable. The literal + and > can't be a bare key token (they are the separators), but those keys are bindable as shift+= and shift+.; only increase_font_size's default ⌘+ shows as a glyph because its stored form doesn't round-trip through the file.
  • The Ctrl-Tab MRU switcher and Ctrl-1/Ctrl-2 pane focus are not rebindable yet.

Ghostty config

agterm builds its terminal config from four sources, each overriding the one before it:

ghostty defaults → ~/.config/ghostty/config → <config dir>/ghostty.conf → agterm Settings
  (lowest)    (global, OFF by default)    (agterm-scoped)    (UI wins)

agterm is self-contained: by default it does not read your global ~/.config/ghostty/config, so a config written for the standalone Ghostty.app never silently changes agterm. Turn on Settings ▸ General ▸ Use my global Ghostty config to fold it in.

<config dir>/ghostty.conf (default ~/.config/agterm/ghostty.conf) is the place to customize agterm. It sits next to keymap.conf, is always loaded, and is scoped to agterm so the standalone Ghostty.app never reads it. Put any ghostty config key there — the keys agterm manages from Settings (font, theme, opacity, blur, scroll speed) still win. A common use: macos-option-as-alt = true. The full key reference is at ghostty.org/docs/config.

A keybind here follows ghostty's own rules, which differ from keymap.conf: a bare letter or digit binds the character the active layout produces, so keybind = super+opt+ctrl+g=text:hello stops firing on a non-Latin layout, where that key types п. Prefix the key with key_ to bind the physical position instead — keybind = super+opt+ctrl+key_g=text:hello works on any layout. agterm's bundled defaults already use it for ⌘C, ⌘V, and ⌘A.

Programs can read and write the macOS clipboard over OSC 52. agterm prompts before a program reads your clipboard (a read hands its contents back to the program); a normal ⌘V paste is never prompted. Clipboard writes go through by default, matching other terminals so a remote tmux/vim yank still reaches your clipboard. To gate writes, set clipboard-write = ask or deny. Each prompt offers Don't ask again this session.

A ⌘-click on a file:// link — the kind ls --hyperlink, eza, and many compilers emit — reveals the file in Finder instead of opening it. A terminal renders untrusted program output, so a link could point at a .app or .command; revealing selects the file without running it, which is the security boundary — actually opening it stays a separate, explicit action. Web (http/https) and mailto links still open as before. A file:// link that names another host is ignored rather than revealed, so a stray link can't trigger a Finder network mount.

Link detection follows the program in the pane. While one has mouse reporting on — tmux with mouse on, or stock vim — ⌘-hover stops underlining and ⌘-click opens nothing, because links are detected only while reporting is off. Same in Ghostty.app, and per-program rather than per-kind of app. Hold shift as well (⌘⇧-hover, ⌘⇧-click) to reach the link anyway, unless the program claimed shift for itself. In ghostty.conf, mouse-shift-capture = never makes shift always win, and mouse-reporting = false turns reporting off for every program.

Open the file with File ▸ Edit ghostty.conf… (a 95% editor overlay, same as Edit Keymap). Apply edits made elsewhere with File ▸ Reload Config or agtermctl config reload. A malformed line is skipped and the good ones still apply; the returned diagnostic count covers every config source.

Agent status

A coding agent running in a session can flag its status on that session's sidebar row, so you can tell at a glance which of many concurrent agents needs you. The glyph shows just left of the notification badge on every non-idle session; a one-time completed flash auto-clears once you visit the session.

active
lavender-grey glyph
blocked
amber glyph
completed
green glyph
idle
nothing

Every state draws a filled circle by default, so the tint is what tells them apart. The shape is configurable per state under Settings ▸ Agent Status ▸ Colors and Shapes, where each state carries its own color well and shape picker on one row. The six shapes are circle (the default), square, triangle, diamond, capsule, and star, each drawn in that state's current color. Choosing a distinct shape per state adds a second signal alongside the tint, so the states stay apart at a peripheral glance and without depending on hue.

An agent sets it over the control channel:

# state is idle | active | completed | blocked
agtermctl session status active --target "$AGTERM_SESSION_ID"
agtermctl session status blocked --sound default --target "$AGTERM_SESSION_ID"
agtermctl session status completed --auto-reset --target "$AGTERM_SESSION_ID"
agtermctl session status blocked --color '#ff0000' --target "$AGTERM_SESSION_ID"
agtermctl session status blocked --shape triangle --target "$AGTERM_SESSION_ID"
agtermctl session status blocked --pane right --target "$AGTERM_SESSION_ID"
agtermctl session status idle --target "$AGTERM_SESSION_ID"  # clear it

--auto-reset clears the indicator the moment you visit the session; --blink requests an attention pulse; macOS Reduce Motion suppresses the repeating sidebar and dashboard animation while keeping the status visible, and the pulse resumes when Reduce Motion is disabled; --sound plays a one-shot sound (default, or a system sound name like Basso/Ping/Tink). To make every blocked prompt sound without touching the hooks, set Settings ▸ Agent Status ▸ Blocked sound. --color (#rrggbb) overrides the glyph tint for that one call — it rides the status, so the next status set without it reverts to the configured color. --shape (circle, square, triangle, diamond, capsule, or star) overrides the silhouette the same way, reverting to the configured shape on the next status set without it. Both read back on tree as the session's statusColor and statusShape, each reporting the per-call override only.

--pane (left|right|scratch, defaulting to the main pane) records which pane set the status: a block set from a background pane survives typing in another pane, and when the status needs attention (blocked or completed), any GUI selection of the session (auto-follow, attention nav ⌃⌥↑/↓, plain session nav, the command palettes, a Dock-menu session, and a sidebar click) reveals and focuses that pane — the split, or a hidden scratch — instead of the main pane, so an agent that blocks or completes in a split or scratch tags its own pane to be found. An active status keeps the existing pane selection (the control session go next-attention only steps the selection, it does not itself move focus into the pane). It reads back on tree as statusPane.

Typing into a blocked/completed session clears its status; an interrupt keystroke, Esc or Ctrl-C, interrupts an active one and clears it too.

When the sidebar is hidden the glyphs go with it, so an optional title-bar bell (Settings ▸ Notifications ▸ Show attention indicator, off by default) reflects the window: dimmed when nothing needs you, plain when a session is active or completed, filled amber when any is blocked. Clicking it opens a popover of this window's non-idle sessions, sorted blocked → active → completed, that you hover to highlight and click to jump to the session. A blocked or completed row also reveals the pane that set its status; an active row keeps the existing pane selection. ⌃⇧I, Navigate ▸ Go to Attention…, or the action palette's "Show Attention" opens the same attention list as a searchable palette. Right-clicking agterm's Dock icon exposes the last-active window's same ordered list under Sessions Needing Attention, alongside its recent sessions. Over the control channel, agtermctl tree --json reports each session's status and statusPane (which pane set it).

Auto-follow blocked sessions. When several agents run at once, a session that blocks is easy to miss. Turn on Settings ▸ Agent Status ▸ Auto-follow blocked sessions (Disabled by default, or a 5s/10s/30s/60s/5m idle timeout) and, after you have been idle from input for that long, the window selects and focuses the oldest waiting blocked session, so you are pulled to whatever agent is waiting. It is per-window and window-wide (crossing workspaces within the window). Auto-follow pulls you to each blocked session at most once: after you have been shown a block and moved on (even without replying), it will not pull you back to it, so it walks the waiting blocks oldest-first, shows each once, then stays quiet. A session becomes eligible again only after it leaves blocked and re-enters it. Being parked on a blocked session likewise suppresses jumps while you stay on it. The opt-in Don't auto-follow away from a running session (off by default) holds the selection put while the current session is active. Over the control channel, agtermctl tree --json reports the window's idleMs (ms since your last input, live) and autoFollowMs (the configured timeout in ms, omitted when Disabled); agtermctl window list --json reports autoFollowMs per window (as of the last refresh), but not the live idleMs.

To wire it up automatically, Help ▸ Install Agent Status Hooks… installs a hooks package: a generic bash/zsh/fish shell integration (flags active while a command matching AGTERM_AGENT_RE runs; the default set is gemini, cursor-agent, aider, crush, goose), four Claude Code hooks (prompt → active, tool run → active, Stop → completed, permission prompt → blocked), and an OpenCode lifecycle plugin.

Six Codex lifecycle events run a dedicated adapter. A permission request is only a candidate because it fires before Auto Review; the adapter keeps automatic decisions active and changes the row to blocked only after a real approval or structured question appears in that pane. On Stop, a final assistant message containing ? reports blocked; every other final message reports completed and auto-resets.

When Pi has already created ~/.pi/agent, the installer also adds a lifecycle extension: Pi work starts as active and settles as completed after retries and queued continuations finish. Pi has no built-in permission or structured-question event, so it does not infer blocked from assistant prose. Restart Pi or run /reload after installing it. Re-running upgrades agterm-managed integrations and is otherwise idempotent.

When OpenCode has already created ~/.config/opencode, the installer also adds a lifecycle plugin at ~/.config/opencode/plugins/agterm-status.js: session.status busy/retry → active, idle → completed (only when no session remains busy, so a task subagent cannot complete a busy parent), permission/question prompts → blocked, turn-ending session.error for a session already reported busy → blocked (following idle from halt swallowed, and a clean sibling's idle cannot erase it; abort ignored, context overflow decided by the next event — busy means compaction resumed, idle means the turn ended → blocked), reply/reject → active. Deprecated session.idle is ignored so it does not double-fire with session.status. Restart OpenCode after installing it.

Restore & persistence

Sessions come back on the next launch with their directory, font size, and split state restored. Restore reconstructs the structure, not the running processes — three limitations follow from that design:

  • Live processes are not reattached. By default a restored session re-spawns a fresh login shell in its saved directory. The optional Restore running commands on restart toggle (General settings, off by default) re-runs the command each pane had in the foreground at the last clean quit — a re-run, not a reattach. Only a single-process command restores faithfully; a force-quit or crash captures nothing, and a capture replays exactly once, since the launch that arms it clears it from the state file; and the multiplexers in restore-denylist.conf (seeded with tmux/screen/zellij) start fresh. A per-session override, agtermctl session restore, pins what a pane restores (or --none for a plain shell, --clear to drop it) — consumed on the next launch, sticky across restarts, winning over both the captured command and the session's own --command, and reading back on tree. It obeys the same setting but bypasses the denylist, since it names its command deliberately. The pinned value is shell code stored in the window's state file and readable via tree, so it must not carry secrets. A SessionStart hook can rewrite it so a non-idempotent command like claude --resume … --fork-session reattaches on restart instead of forking.
  • The saved directory depends on OSC 7. It relies on Ghostty shell-integration (auto-injected for zsh, bash, fish, and nu). If the working directory is never reported, a session restores to the directory it was created in.
  • Directory is saved on structural changes, not every cd. It persists on quit and on each add/close/move/rename/select — not on every prompt redraw, which would thrash the disk. A crash loses only the directory changes made since the last structural change or quit.

Troubleshooting

Where the logs and config live, how to read them, and the common problems (a keymap editor that won't open, a custom action that does nothing, missing notifications) are covered in the repo's troubleshooting guide.

troubleshooting.md ↗ Open an issue ↗ Start a discussion ↗
agterm embeds libghostty, the terminal engine from Ghostty (MIT) — rendering, VT parsing, and shell I/O, built from upstream source at a pinned commit. The way agterm drives libghostty's C API from a SwiftUI/AppKit app was learned from macterm (MIT), and SwiftUI guidance came from the SwiftUI Agent Skill (MIT). The model, sidebar, persistence, control channel, and multi-window code are original to agterm.
← agterm.com
GitHub Issues Discussions