> ## Documentation Index
> Fetch the complete documentation index at: https://tty7.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Command reference

> 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 <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-<char>` (Ctrl, e.g. `C-c`), `M-<char>` (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"}`                                                                                                         |

<Warning>
  `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.
</Warning>

Prefer `tty7 new <path>` 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: `@<uuid>`.

| 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.

<Warning>
  `--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.
</Warning>

`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                                                                                         |

<Warning>
  Do not run `start`, `stop`, or `restart` on someone else's behalf. They change
  or destroy what the user's GUI is attached to.
</Warning>

## 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
