CLI¶
taw agent start, taw agent prompt --wait, taw agent wait --state blocked, all JSON, plus an agent skill (taw skill).
Every command prints a JSON envelope to stdout, except eight. taw skill and
taw config default print raw text, because they exist to be redirected into a file.
taw open is for people at a shell: it prints nothing when it works (--json gives it the
envelope), and a line on stderr when it does not. So is taw sandbox [<agent>], the start: it
becomes the profile when it works, and prints one line on stderr when it does not (taw sandbox
new and taw sandbox list print the envelope). taw app is a screen and prints none. taw machine feed prints JSON lines, for the app to read. taw machine attach becomes tmux on that machine, and taw machine attach --new starts a session on one that has none (a target and --new are exclusive, and it needs a terminal, refused with status 2 without one; the app runs it for Enter on a machine's no tmux session here row, and taw app --go studio:new is the same from the popup's doorway), and taw machine check prints one line a machine (--json gives the envelope). taw machine list, taw machine add and taw machine remove print the envelope.
taw --machine NAME <command> runs a command of the list on a machine under [machines] and prints its answer as it was given; machine_unreachable is the error code when no answer came, and the ids are that machine's (see "Remote machines").
{"id":"cli:pane:list","result":{"type":"pane_list","panes":[…]}}
{"id":"cli:pane:get","error":{"code":"not_found","message":"pane 'w9:p9' not found"}}
Groups: agent, pane, workspace, tab, worktree, layout, notification,
hooks, session, integration, plugin, manifest, terminal, watch, seen,
status, open, ui, popup, config, skill, sandbox, app, machine, image.
taw image paste prints the envelope, with the pane the image went to.
Targets accept a public id (w3, w3:t7, w3:p31), a raw tmux id ($3, @7,
%31), a tmux target (session:window.pane), or, for agent commands, a live
agent name. One edge case worth knowing: a tmux session literally named like a
public id (say, a session named w5) still resolves the bare string w5 as the
public id of session $5, never as that session by name; address it as $N
(its raw tmux id) instead. See docs/ARCHITECTURE.md "ID mapping" for why.
The verbs behind the two bound keys: taw ui open reads the client's size and status
line, computes the popup's geometry, and runs display-popup for taw ui (which is
the TUI itself, and runs in whatever terminal you start it in). When the launcher swapped
the status line for taw's bar, that popup command also carries --bar, so only that
popup keeps @agent_wizard_bar fresh: a taw ui run any other way (typed in a pane, or
opened without a swap) never touches it. taw notification
open-target goes next up, reporting {"pane_id": …, "target": "needs_you" | "idle" |
"rest"} (target is the group of the pane it went to: the next agent that needs you after the
current pane, in tmux order, else the next agent of any kind) or a not_found when no
pane holds an agent.
taw session open <path> switches to the session already rooted at <path>, or
creates one: the picker's Enter, as a command. taw open [terms] is that for a person at
a shell, taking a directory or zoxide terms and attaching the terminal outside tmux. It
runs the create hooks as session open does, so a pre-create hook that refuses stops it
with nothing created (hook_refused under --json). With no terms it runs
taw ui open --new-session, which opens the popup on the new-session picker
(taw ui --new-session does the same for the popup itself), and Esc closes it. The
picker n opens inside the popup is different: Esc there goes back to the tree.
taw sandbox [<agent>] [--profile <name>] [-- <agent args>] runs an agent in a container, in the
pane it is typed in; taw sandbox new <name> [--from <profile>] copies a profile into your folder
and prints its path, and taw sandbox list prints the profiles taw can run, and the ones it
would run but refuses, with why (see "Agents in a sandbox").
taw agent start <name> --kind <kind> --pane <pane> [--timeout <ms>] [--sandbox [--profile <name>]]
[-- <agent args>] types the agent's command into an existing pane, and only once that pane's shell is at its
prompt: nothing in front of the terminal but the shell, something drawn, its screen
unchanged for 300ms. A pane already at its prompt is typed into at once. A shell that is
still starting would hand the keystrokes to whatever reads the terminal first, such as a
question its startup asks. --timeout (default 30000, at most 300000; with --sandbox, 300000)
is one budget for the whole verb (a look at the pane already under way when it runs out is
finished, a second or two on a busy machine): the wait for the shell counts against it, and the agent has what
is left. A shell gets 15 seconds of it, or all of it when that is less; one that has not come
to its prompt by then fails the verb with the error code shell_not_ready, nothing was
typed, and the pane is not marked as holding an agent. A pane in copy mode (or any other mode)
or with its input turned off (select-pane -d) fails it at once with the same code and the
reason ("pane w1:p3 is in copy mode; leave it and try again"), since tmux would give what is
typed to the mode, or to nothing; the pane is left in its mode, where you may be reading the
scrollback, and is not marked either. One put in a mode after the command is typed and before
its Enter goes is refused the same way, and the message adds that the command may be left on its
line, which taw does not clear without taking the pane out of its mode. The command goes as text,
and its Enter after it, each only while the shell the wait found at its prompt is still in front of
the pane, which tmux is asked in the call that sends, as a restore sends its own: a program started
in the pane in between (one you start, or one a key binding runs, such as a history search) is sent
nothing, and the verb fails with shell_not_ready and says so ("pane w1:p3 had another program in
front of its shell when the command was to be typed, so nothing was typed"), adding that the
command may be left on its line when the program came in front after the command (or after part of
a very long one) and before its Enter. A key binding of your shell's that runs a program of its own directly on every key typed (a
zsh widget or a bash bind -x that runs it as a command, not inside $(...)) has that program in
front for a moment, and an Enter that comes then is held back the same way. A prompt such as starship's
or oh-my-zsh's, and the bindings of atuin and fzf (C-r, Up, C-t, Alt-c and Tab, none of which the typed
line sends), are not that. Never typed into,
because taw cannot tell it from a shell that is not ready: a pane whose process is not itself a shell
(ssh, docker exec, a wrapper that does not exec); a shell that draws nothing, such as an
empty prompt (PS1='') in a blank pane; and a question the shell asks itself with a
read of its own (zsh's compinit question about insecure directories, the oh-my-zsh
update prompt). The check runs ps -o pgid=,tpgid=,comm= and pgrep -g <group> (the members of the terminal's foreground group, a process's own ancestors included; macOS needs -a for that), which macOS and
Linux (procps) have; a ps with no tpgid column cannot tell, and then nothing is typed.
The verb returns once the agent is really there, and not when its command is typed. The pane's
own shell must be no longer in front of the terminal, and either the process in front is named
for the agent (a program called claude), or the agent's own integration has reported it
(pane report-agent-session, or a state a hook wrote), or the pane's watcher has read a state
for it off the screen with its kind's rules, which is any state but unknown. A pane still
shows what its last agent left (an agent that died at a permission prompt leaves the prompt), and
what the command starts can draw only below the line it was typed on, so the verb records that
line (@agent_typed_line) once the shell has echoed the command on it, before it sends Enter (a
shell that has not echoed it within a second has the line recorded where the command was typed),
and the watcher reads only the rows below it while it is on the screen (a row is that line when it
has its shape: the prompt as it was drawn, or another put in its place no wider than it or than 16
columns, the whole command, and after it a right prompt only if the prompt row showed one before the
command was typed; a row of the agent's own that names the model, a tip, or its footer saying to
Run: mise upgrade claude, is not): the state counts once something is drawn there. The rows a
shell rewrites when the command is entered (a transient prompt, as powerlevel10k offers) are that
line and the rows above it (the rewritten row holds the command, even when it keeps a right prompt,
as zsh does unless TRANSIENT_RPROMPT is set, or when it takes a command that had wrapped back to
one row), and what the command prints before the agent draws (an install's progress) is read for
what it is, which is unknown, with no notification, until a rule of the agent's kind matches, for a
kind whose rules know its idle screen (an agent whose rules know only its work and its questions, such
as OpenCode or Gemini, reads idle at any frame no rule knows, there as anywhere). The title the last
agent left is not read as the new one's until tmux reports another, even when the new one sets the
same title (Codex sets its folder's name): an agent whose idle only its title shows (Amp, Hermes, and
Codex with [detection] supplements = false) reads idle at any frame no rule knows once it has drawn,
as OpenCode does. A watcher started again (a plugin reload or an upgrade) reads an agent that drew
before it as it did, so a done stays. Once the agent's own drawing has pushed the line off the
screen or cleared it, and once the agent exits, the whole screen is read again; a read that does not find the
line reads nothing of the screen and keeps what the pane says, and the next read, made at once, takes
the line for gone if it does not find it either. An
agent that runs under node or a wrapper script is told by its own integration's report, or by
the watcher, which the tmux plugin attaches to every pane and which takes the 3 seconds a new
agent reads unknown. What the pane recorded of its last agent (its report, its state, its
session and its name, and the pid agent start recorded for it) is taken off before the command
is typed, so a report the last agent left is not this agent saying it started, and a second start
in the pane of an agent that died is a new agent to the watcher, with the same 3 seconds when the
watcher's first read after the start finds nothing in front of the pane naming it yet, and with none
when its process names it at once, so that it is read from the rows below its line from that first
read on. The pid
of the agent's process is recorded as soon as the job in front of the pane names the agent (by the
names or the arguments of its processes); for an agent nothing names, it is the process in front
that the verb's last two looks agree on, recorded once the wait is over (when the wait's last two did
not agree, it looks again for up to 1.5 s, three times at the least however long a look takes on a busy
machine, and past that until two agree or until the sixth, but begins none once --timeout has run
out, and then records the pid only if its last two looks agree), so a helper your shell runs before the command (a preexec hook's) is
not taken for the agent, and a verb that ends in timeout leaves the pid of what came up for the
watcher. A shell that is back at its prompt before the
agent is there (the command was not found, or the agent exited at once) fails the verb at once
with the error code agent_not_started, and the pane's @agent_kind_hint and recorded pid go
back to what they were; the budget running out is the error code timeout. A shell whose prompt
starts at the left edge of the screen is taken for one that is still working, and ends in
timeout; a question the shell asks of its own after the command was typed (a read) cannot be
told from its prompt, and is taken for it. A name that another pane's live agent already has
fails the verb with invalid_argument, as agent rename does, before the wait for the shell and
before anything is typed; a pane whose own shell is at its prompt holds no live agent, whatever it
records.
taw agent wait and agent prompt --wait end with agent_not_running when the agent has gone
or another has taken its pane. For an agent started this way, whose pane is named by its hint
alone, that includes a process that has exited (the pid recorded for it is dead), and for any agent
a pane whose own shell is back at its prompt with nothing in front that identifies as the agent,
which is what ends a wait for an agent you started by hand when no watcher takes its record off.
A turn such an agent died in the middle of ends as done, or idle when you were looking, and a
wait that wanted that status returns it. An agent reading idle is not permission to approve anything,
and a wait watches the agent's state, not its background jobs: automation that trusts a mistaken
idle can type into an approval dialog, and an agent at its prompt reads idle while the commands,
monitors and subagents it started keep running.
taw agent prompt and taw agent send-keys refuse a pane that no longer hosts its agent before
anything is typed, since the text would land at the shell's prompt and a shell may run it: with
herdr's agent_not_ready for a pane that hosts no agent or whose agent has gone, and
agent_not_found for a target that resolves to no pane. A pane hosts its agent only while the job in
front of its shell is still that agent: the processes in front of it, with their arguments, have to
name the agent (node /path/to/bin/codex is codex), so a shell at its prompt, a shell you started
inside the pane (sh, ssh, nix develop, sudo -s), a pager, an editor or cat hosts none,
whatever the pane records: an agent you started by hand that has exited is refused, and so is a
pane where you have started something else since. The send itself is guarded, in the one tmux call that
makes it, on the command in front of the pane being the one that was checked, so an agent that exits
between the check and the send is typed nothing either. A pane in copy mode (or any other mode) or
with its input off is refused with shell_not_ready and the reason, as agent start refuses one,
and left in its mode: tmux would give the text to the mode, and the agent would never see it. The
guard asks that too, so a pane you put in copy mode between the check and the send is refused the
same way, and a prompt whose text had gone before the pane went into the mode says the prompt may be
left on the agent's line.
taw watch status lists every pane with whether it is piped and whether its watcher is
alive, and marks each live watcher whose taw is not the one installed (binary, with
binary_note for an old one, the count of those --restart would start in old and the
command to run in restart). taw watch attach-all --restart attaches a watcher to every pane
that has none, as attach-all does, and starts each old watcher again as well, counting the
ones it could not start in failed (see Troubleshooting).
Verbs added for the last herdr socket methods (all return the JSON envelope, --pretty
accepted): taw layout export [tab] / apply <tab> <layout> / set-split-ratio <pane> <ratio>
(a tab's #{window_layout} as herdr's split tree; apply needs a single-pane tab);
taw terminal title set <text> / clear (the outer terminal window title via tmux
set-titles, clear restores the previous string); taw popup close (closes the agents
popup on the caller's client); taw session snapshot (the live workspaces/tabs/panes/agents
snapshot); taw config reload and taw manifest reload (force every live watcher to re-read
config.toml and the local manifest overrides; both are also picked up automatically when
their files change); taw config reset-keys (drops the [keys] table, other bytes untouched);
taw agent view set --source <id> [--label <l>] --filter <json> [--sort <json>] / clear
(a declarative projection of the popup's agent rows, stored in @agent_wizard_view).
Clearing a pane¶
taw pane clear [<pane>] (also --pane <id> and --current; the pane it runs in when
none is named) is herdr's pane.clear, which herdr offers over its socket and as a key
(keys.clear_pane), unbound until you set it. It empties a pane's scrollback and, where a
shell is waiting for you, its screen:
-
On the alternate screen (what a full-screen program such as Vim or
lessdraws on) it changes nothing, as herdr's does not. -
Anywhere else it clears the pane's history. A program in front of the pane, an agent, an editor or a build, keeps its screen and is sent nothing.
-
At a shell's prompt it clears the screen too. When bash, zsh or fish is at its prompt, with nothing else in front of it, the shell is sent Ctrl-L: it clears its screen and draws its prompt and the line you were typing at the top, the line herdr keeps, and the old screen, which tmux moves into the history as the shell clears it, goes from the history as well. The verb returns once the shell has drawn it, normally in a fraction of a second, and says the screen was cleared only when it shows it (the old screen gone into the history, or the cursor's row moved up); a shell that shows neither within three seconds did not answer, and the note says so. A screen that is clear already, the prompt at the top and nothing else on it, is not asked for anything, and the reply says it is cleared. Any other shell, such as a plain
sh, which would read the key as text, is sent none. On macOS/bin/shis a bash and tmux calls it one, so a pane running it is sent the key. -
In a window with
synchronize-paneson the key is not sent: tmux would copy it to every pane of the window, a program in front of one of them included. The history is cleared and the screen stays, and the reply's note says why.
tmux can clear a pane's history but not its screen, which is the program's own, so asking
the shell is the one way to a clean screen that does not reset the pane's terminal and undo
the modes a program set. The reply is the usual envelope, with a note when something was
left alone: {"id":"cli:pane:clear","result":{"type":"ok","pane_id":"w1:p2","history_cleared":true,"screen_cleared":true}}.
set -g @agent-wizard-clear-key C-k binds it on the prefix, for the pane you press it in
(prefix + C-k); a space-separated list binds each, and with the option empty nothing is
bound. For a key without the prefix bind it yourself, as bind -n M-k run-shell -b "taw pane
clear '#{pane_id}'" (with the full path to taw if tmux cannot find it): run-shell has no
$TMUX_PANE, so the pane is named in the binding.
What it cannot tell, and what it does about it:
-
A shell is at its prompt when the process table says nothing but that shell has the terminal and its screen has held still for 300ms, which is the test
taw agent startuses. A shell that is waiting in a builtin of its own (read, zsh'sselect) passes it and is sent Ctrl-L as input, which the terminal echoes as^L. -
A bash in readline's vi mode takes Ctrl-L at its prompt as a character, and inserts a literal
^Linto the line; zsh in vi mode does not. Nothing outside the shell says which mode it is in, so the verb looks for a^Lthat is on the screen now and was not, wherever it landed (the end of the line, the middle, wrapped past the right edge, after wide characters, on a blank last row), and takes it out again with one Backspace and leaves the screen alone (screen_clearedisfalse, with a note; if the Backspace does not take it out, the note says that). To have bash clear in vi mode, bind it in~/.inputrc, afterset editing-mode vi:set keymap vi-insertand then"\C-l": clear-screen. -
A shell that has Ctrl-L bound to something else gets that.
- bash and zsh are tried here. fish is matched by name and sent the same key, but real fish's Ctrl-L has not been tried.