--- title: "Command reference" description: "Every verb, its flags, and the JSON it emits under --json." --- ## Global flags Accepted anywhere on the line, before or after the subcommand. | Flag | Effect | |---|---| | `-m, --machine ` | Route to a linked machine over the local server's existing link. Matches the full link key (`me@devbox:22`) or the bare host (`devbox`). SSH links only; a down link, or a jump/proxy chain, is refused with a reason rather than dialled fresh. | | `--json` | One JSON object on stdout instead of the human table. | | `-q, --quiet` | No output on success. Errors still go to stderr. | ## Environment Set inside every tty7 pane, inherited by anything launched from one. | Variable | Meaning | |---|---| | `TTY7_PANE` | This pane's id, e.g. `71` or `%71` (both accepted). Default target of `split`, `send`, `capture`, `procs`, `wait`, `pane close`. | | `TTY7_WS` | This pane's workspace id. Default for `run --keep`, `tab new`, `tab ls`, `ws tree`. | | `TTY7_CONFIG_DIR` | The server's config dir — how the CLI finds the right server. You never pass a socket path. | Outside a tty7 shell, address-taking verbs fail with `not inside a tty7 shell — pass an explicit %pane/@tab/workspace`. ## Exit codes | Code | Meaning | |---|---| | `0` | Success | | `1` | The command failed; one line on stderr, prefixed `tty7:` | | `2` | Usage error — unknown verb, missing argument, bad type | | `124` | `tty7 wait` timed out (the `timeout(1)` convention) | | `141` | Unix only: the reader hung up — piping into `head -1`, say — and SIGPIPE ended it, exactly as it ends `cat`. Not a failure. Windows reports 0 for the same thing, having no signal to imitate. | | *other* | Only from `tty7 run`, which passes the child's exit code through | If `run` cannot learn the child's code it prints a note to stderr and exits 1 with `"exit_code_known": false` in the JSON — that is how you tell a real 1 from a stand-in. ## Top-level verbs ### `tty7 [PATH]` No subcommand means the GUI. A running window is asked to come forward and open a tab at `PATH`; if none is registered, the app is launched instead. JSON: `{"path","delivered","launched"}` — `delivered` says an existing window took it, `launched` that a new process was started. Without `PATH` it just activates the app. `-m` is refused: this verb drives the GUI on *this* machine. ### `tty7 ls` Same as `ws ls`. Table: `WORKSPACE NAME TABS PANES ATTACHED`. JSON: `{"workspaces":[{"id","name","tabs","panes","attached"}]}`. `ATTACHED` names the host holding the workspace — a GUI window, or another client — and is `-` when nobody is. ### `tty7 run [--keep] [--cwd DIR] [--ws WORKSPACE] -- CMD...` Spawns a pane running `CMD`, streams its output to stdout, waits, and exits with its code. The command must come after `--`. - `--keep` leaves the pane alive as a new tab afterwards. Needs a workspace, so it requires `--ws` or `$TTY7_WS` — without one it is an error, not a silent fallback. - `--cwd` sets the working directory. `--ws` also sets the pane's `TTY7_WS`. - Interrupting `run` can leave the pane behind as an orphan — see `pane ls --all`. JSON: `{"pane","exit","exit_code_known","kept"}`, printed **after** the streamed output. The combined stream is not valid JSON — read the last line. ### `tty7 new [PATH] [--open]` Creates a workspace plus its first tab and shell, at `PATH` if given. Prints the workspace id. JSON: `{"id","pane","opened"}`. `--open` also puts a window on it, if a GUI is running on this machine. Without it the workspace still appears in the switcher; it just waits to be opened. ### `tty7 split [%PANE] (--v|--h) [--ratio R]` Alias of `pane split`. Splits `%PANE` (default `$TTY7_PANE`), spawning a shell in the same cwd. Exactly one axis is required — `--v`/`--vertical` puts the new pane below, `--h`/`--horizontal` to the right. `--ratio` (default `0.5`) is the share kept by the *existing* pane, clamped to `0.05`–`0.95` — a `--ratio 70` silently becomes `0.95`, not an error. Prints `%NN`. JSON: `{"pane"}`. ### `tty7 send [%PANE] [TEXT] [--enter] [--key KEY]…` Types `TEXT` into the pane as keystrokes; `--enter` is shorthand for `--key enter` — it appends CR to the text, or presses Enter on its own when there is none, so `tty7 send %42 --enter` runs whatever pane 42 already has typed. With one argument the text is the argument and the pane comes from `$TTY7_PANE` — but a lone `%42` (or bare `42`, the shape `pane ls --json` prints) is rejected as a missing-text error rather than typed, unless a `--key` gives it something to do. `--enter` is that key only for the `%`-marked spelling: `tty7 send 83 --enter` is refused, because it reads as much like typing `83` into your own pane as like pressing Enter in pane 83, and the error names both ways to say which (`send %83 --enter`, `send %PANE 83 --enter`). A `%` followed by a digit that still doesn't parse (`%3x`) is an address error, never text for your own pane — while text that merely starts with `%` (`%s/foo/bar/`, `%!sort`) types as given, as does anything unmarked that is not a plain number (`3x`, `+5`). To type an address-shaped string, name the pane as well: `tty7 send %42 %3x`. `--key` presses a key instead of typing characters, which is what a pane wants once something is already running in it: answering a prompt that only takes arrow keys, closing a TUI with `escape`, stopping a build with `C-c`. Repeat it for a sequence, and it composes with `TEXT` — the text goes first. | | | |---|---| | Named | `enter` `escape` `tab` `backtab` `space` `backspace` `delete` `up` `down` `right` `left` `home` `end` `pageup` `pagedown` | | Chords | `C-` (Ctrl, e.g. `C-c`), `M-` (Alt) | | Aliases | `return` `cr` `esc` `del` `bs` `shift-tab` `pgup` `pgdn` `pgdown` | Names are case-insensitive, and an unknown one is a usage error (exit `2`) raised before anything is sent — half a key sequence in a live pane is worse than none. One case exception: Alt is a prefixed ESC, so its character goes out exactly as written and `M-X` is not `M-x` (Ctrl is unaffected — `C-c` and `C-C` are the same byte). Each keystroke is delivered as its own event, 200 ms apart, so a raw-mode TUI reads a sequence as a sequence rather than as a paste. JSON: `{"pane","sent","enter","keys"}`. ### `tty7 capture [%PANE] [--plain] [--scrollback]` The pane's replay. Two independent choices: **How much** — the newest scrollback segment by default, the whole ring with `--scrollback`. The ring splits into segments on resize, so for a pane that was never resized the two are identical. **In what form** — without `--plain`, the stored bytes with ANSI escapes intact, decoded as UTF-8 (invalid bytes become U+FFFD). With `--plain`, those bytes replayed through a terminal grid and printed as the text they produced. Either way it is a snapshot, not a stream: it collects the replay, settles for ~300 ms, and returns. Call it again for a newer one. JSON: `{"pane","text"}`. ### `tty7 procs [%PANE]` The process tree inside the pane, indented by depth, `*` on the foreground process — then a second table of ports those processes are listening on. Prints `nothing running in this pane` when both are empty. JSON: `{"procs":[{"pid","name","depth","foreground"}],"ports":[{"port","pid","name","addr"}]}` — `addr` is the address the socket is bound to (`*`, `0.0.0.0`, `127.0.0.1`, `[::1]`, or a specific interface). ### `tty7 agents` Every pane running a recognised coding agent. Table: `PANE AGENT STATUS MESSAGE`, status one of `idle` / `working` / `waiting` / `done`. JSON: `{"agents":[...]}`, plus a `"diagnostics"` array when an agent's status hook is missing or out of date — that is why an agent can be listed with a status that never moves. ### `tty7 wait [%PANE] [--until STATE,…] [--changed] [--timeout SECS] [--interval MS]` Blocks until the pane reaches one of the named states. | Flag | Default | | |---|---|---| | `--until` | `waiting,done,exit` | See the state table below | | `--changed` | off | Only wake on a state the pane moved into *after* the wait began | | `--timeout` | none | Give up after N seconds, exiting `124` | | `--interval` | `500` | Poll interval in ms (50–3,600,000) | The states come from two places. Four are the agent's own status, as reported by its [hooks](/agents/status); the last three are facts about the pane: | State | Means | |---|---| | `idle` `working` `waiting` `done` | The agent's status | | `no-agent` | Nothing is reporting status here — a plain shell, or an agent whose hooks are not installed | | `free` | The foreground command has exited; the pane is back to its bare shell | | `exit` | The pane itself is gone. Ends every wait whether it was asked for or not | `free` is how you wait for a **command** rather than an agent, and it is the one state that costs a second request per poll — so it is only checked when you name it, and only when none of the agent states you asked for already matched. With `--changed` it means "something ran and then finished", which is what you want directly after a `send`; a command quick enough to finish inside one `--interval` is never seen running, and the timeout says so. The reply carries the agent's message and native session id. The JSON's `stale` flag says whether the answer might belong to the previous turn. JSON: `{"pane","status","matched","stale","activity","message","session_id"}`. A timeout exits `124` with the same object plus `"timed_out": true` — `matched` is `false` there, and `stale` still says whether the pane moved while you watched. [Orchestration →](/agents/orchestration) ### `tty7 events` Streams server events until interrupted, one per line — pane exits, agent status changes, workspace preemption, layout deltas. `--json` makes it NDJSON. Blocks forever; run it with a timeout or in the background. ### `tty7 status` Same as `server status`: pid, uptime, pane count, dialect versions, build, socket path. JSON is the `ServerStatus` object itself (`pid`, `uptime_secs`, `panes`, `control_version`, `protocol_version`, `build`, `socket`). ### `tty7 doctor` The install check: the three environment variables, whether the server answers, whether its control and protocol versions match this binary, pid/uptime/panes, how many machine links exist, and where each agent's [status hooks](/agents/status) stand. Adds a note when you are not inside a tty7 shell. The hooks row is the one that explains a mystery: without them an agent reports nothing, so `tty7 agents` shows it standing still and `tty7 wait` sits there until it times out. Outdated hooks fail the same quiet way. Hooks are a local install, so under `-m` the row reads `unknown`. JSON: `{"context":{"config_dir","workspace","pane"},"server":{"reachable","dialect_ok","build","status","routes"},"hooks":{"installed","outdated","not_installed"}}` — the context fields are booleans, not values, and each `hooks` field is a list of agent slugs. ## `ws` — workspaces Address a workspace by name, by full id, or by a unique id prefix (the 8-char prefix `tty7 ls` prints). An ambiguous name or prefix is an error that lists the candidates. | Command | Effect | JSON | |---|---|---| | `ws ls` | Every workspace | `{"workspaces":[...]}` | | `ws tree [WORKSPACE]` | One workspace as a tree: tabs, split axes and ratios, panes with cwds | The whole workspace object: `{"id","name","last_active","active_tab","tabs":[{"id","name","sidebar_group","root",…}]}` | | `ws new [NAME]` | An empty workspace (no tab, no pane) | `{"id","name"}` | | `ws rename WORKSPACE NAME` | Name or rename | `{"id","name"}` | | `ws rm WORKSPACE` | Delete the workspace and hang up its panes | `{"removed"}` | | `ws attach WORKSPACE` | Become its controlling client | `{"attached","took_over_from"}` | | `ws detach WORKSPACE` | Let go without interrupting anything | `{"detached"}` | `ws rm` hangs up the panes the workspace held. If the command reports that some panes could not be hung up, they keep running as orphans with no workspace — find them with `pane ls --all` and close them one by one. Prefer `tty7 new ` over `ws new` when you want something usable: `ws new` leaves an empty workspace you then have to populate, while `tty7 new --json` hands back both ids at once. The `root` node in `ws tree --json` is externally tagged, so a leaf is `{"Leaf":{"pane":31}}` and a split is `{"Split":{"axis","ratio","a","b"}}` with `a`/`b` nested the same way. ## `tab` — tabs `@N` numbers tabs across the **whole machine** in tree order, densely from `@1`. The numbering shifts whenever any workspace or tab is created or removed, so resolve it immediately before use. A full tab UUID also works: `@`. | Command | Effect | JSON | |---|---|---| | `tab ls [WORKSPACE]` | Tabs of a workspace | `{"workspace","tabs":[{"ordinal","id","name","label","agent","group","panes":[…]}]}` | | `tab new [WORKSPACE] [--cwd DIR]` | Add a tab with a fresh shell | `{"tab","pane"}` | | `tab close @TAB` | Close the tab and every pane in it | `{"closed"}` | | `tab rename @TAB NAME` | Name or rename | `{"tab","name"}` | | `tab move @TAB INDEX` | Reposition within its workspace | `{"tab","to"}` | `GROUP` is the heading the GUI's sidebar files the tab under, shown by its last segment. Read-only from here: with the default repo grouping the GUI recomputes it from the tab's working directory. `label` falls back through the best evidence available — the name if someone set one, else the agent running there, else the last segment of the cwd, else the foreground process. `name` stays literal, so a script can tell a real name from a stand-in. ## `pane` — panes | Command | Effect | JSON | |---|---|---| | `pane ls [WORKSPACE]` | Panes with their workspace, tab, cwd, live flag | `{"panes":[…]}` | | `pane ls --all` | The server's whole pane registry, including orphans | `{"panes":[…],"orphans":N}` | | `pane split …` | Identical to top-level `split` | `{"pane"}` | | `pane close [%PANE…]` | Close panes; their shells are hung up | `{"closed":[…]}` | | `pane close --orphans` | Close every pane no workspace holds | `{"closed":[…]}` | `--all` is the one that shows leaks. Each entry is `{"pane","workspace","orphan","owner","title","cwd","live"}`: `owner` is the id of the workspace that may attach to the pane (absent when none may), and `orphan: true` means no workspace holds it. An interrupted `run` leaves orphans here, as does a `ws rm` that reported panes it could not hang up. `--orphans` is the reaper for exactly those. It closes what `pane ls --all` lists as orphaned and nothing else — panes a workspace holds are untouched — and reports an empty list rather than an error when there is nothing to clean up, so a script does not have to guard it. A pane that cannot be closed does not abandon the rest of the batch: the rest are still attempted, the complaint goes to stderr, and the verb exits 1 with `{"closed":[…],"failed":[…]}` — the list a retry needs. `--orphans` closes every orphan on the machine, and an orphan can still be doing real work — an interrupted `run` leaves the command running. Look at `pane ls --all` first. `title` is usually the running command — `claude`, `nvim`, `cargo` — which makes `pane ls --all --json` a quick way to find "the pane running X". ## `machine` — remotes `machine ls` lists the local machine plus every link the server holds: `MACHINE KIND CONNECTED`. JSON: `{"machines":[{"key","kind","connected"}]}`. ## `server` — the daemon | Command | Effect | |---|---| | `server status` | Same as `tty7 status` | | `server logs` | Tail the server log; prints the path, and says so when logging was never enabled (`TTY7_LOG=info` before the server starts) | | `server start` | Bring up a server on this machine | | `server stop` | Stop it — **every pane on the machine dies** | | `server restart` | Stop, then start — same consequence | Do not run `start`, `stop`, or `restart` on someone else's behalf. They change or destroy what the user's GUI is attached to. ## Not implemented yet These parse and then exit 1 with an explanation: - `ws stop` — the control dialect has no workspace-stop request yet - `machine connect` / `machine disconnect` — use the GUI's connection manager