Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Diagnose with tma doctor

tma doctor is the one command to run when a pane is not showing what you expect. It is read-only: it identifies panes exactly the way the poll cycle does, reports each one’s effective tier and why, and never stamps anything.

$ tma doctor
daemon:  not running (/tmp/tma/7f665a9304f7e8ed.sock): tier 3 needs a running daemon (`tma daemon --ensure`)
ambient: polling: `tma status` last ran 0.1s ago
clients: none attached: `#()` status jobs only run while a client draws the status line, so nothing polls this server (run the daemon or attach a client)
watch:   no watcher running (`tma watch` advertises for SIGUSR1 nudges)
hooks:   after-select-pane ✓  session-window-changed ✓
wrapper: /home/you/.local/bin/tma-hook ✓
agents:  6 loaded, no issues
actions: 4 loaded, no issues
remote:  1 pane(s) behind a remote shell: an agent there reports only if it can reach this tmux socket (see docs/how-to/agents-in-containers.md)
  - %10 work:4.0 (ssh)
ignored: 1 pane(s) excluded from detection: unset the option to bring one back (`tmux set-option -pu -t <pane> @agent_ignore`)
  - %1 work:1.0 (ignored via @agent_ignore = manual)

panes (2):
  %7   claude     work:2.0     tier 2   unknown (process, 0.1s ago)
       hooks: wired
       not tier 3: daemon not running (events direct-stamp; run `tma daemon --ensure` for the daemon tier)
  %8   codex      work:3.0     tier 1   unknown (process, 0.1s ago)
       hooks: not installed
       not tier 2: hooks not installed for codex (run `tma install-hooks codex`)

The server-wide lines

The block above the blank line is about the server, not any one pane. Four lines are always there and the rest appear only when they have something to say.

daemon: whether a tier-3 daemon is alive for this server, and the socket it looked at. A daemon running a different build than the CLI adds a second line: tma reload only re-reads config and manifests, so picking up a new build means stopping the daemon and running tma daemon --ensure again.

ambient: whether anything is calling tma status. polling: last ran Ns ago means a driver is alive, whether that is #(tma status) in status-right, an external bar, or a cron job. NOT polling means nothing is, and with no daemon that leaves pane state as stale as your last explicit command. See Show agents in your status line and Drive an external bar.

clients: how many clients are attached. A detached server is a warning only when no daemon is covering for it, because #() status jobs run only while a client is drawing the status line.

watch: how many tma watch instances are running, which is what receives the focus-change nudge.

hooks: and wrapper: the tmux server hooks and the tma-hook wrapper. A hook can read , or as drifted (it runs a different command than this build installs, usually a moved binary), wiped (recorded but gone server-wide, so the server restarted), or missing. Each non-present hook gets its own indented reason line. wiped is the one the daemon repairs itself: tmux hooks live in the server, so a restart drops them, and a daemon re-arms whatever the install record names when it starts. Seeing wiped therefore means no daemon has started since the restart, and tma daemon --ensure fixes it as surely as re-running tma install-hooks.

agents: and actions: the manifest and action rosters, with one - line per file the loader skipped and per action naming an unknown agent. A process_names entry longer than 15 characters is called out here too: that is the width both macOS libproc and the Linux kernel truncate comm to, so such an entry can never match a pane unless a truncated spelling sits beside it.

Four more appear only when the condition holds: status: (the global status option is off, which kills both #(tma status) and display-message notifications), mouse: (the clickable bindings are installed but mouse is off), notify: (your [notify] command failed, with the reason and a pointer to tma debug notify-test), and procs: (the ps walk itself failed, so detection cannot see what runs in a pane and only hook-registered panes are listed below).

The per-pane block

Each agent pane gets a header line and one or more continuation lines:

  %7   claude     work:2.0     tier 2   unknown (process, 0.1s ago)
       hooks: wired
       not tier 3: daemon not running (events direct-stamp; run `tma daemon --ensure` for the daemon tier)

The header is pane id, agent, locator, tier, and the current stamp: state, the evidence source it came from (hook, capture, or process), and how long ago that evidence was taken. A pane with no decodable stamp reads unstamped.

The hooks: line reads wired when every channel names tma’s own entry, and wired (agent codex: notify chained through <program>) when another tool has taken codex’s single notify key and passes tma’s command on to it: the wiring fires, so it is reported rather than warned about.

The tier is what the pane is actually getting, not what it could get:

tiermeans
3Hooks are on the hook path and a daemon is running. Nothing to improve; no reason line is printed.
2Hooks are wired but no daemon is running, so events stamp directly instead of going through the hub.
1The pane is not on the hook path at all: screen and process detection only.

Below tier 3 the block ends with a not tier N: <reason> line naming the next tier up and what it would take. At tier 1 that reason is one of three: hooks are not installed for this agent, the agent is hookless (screen detection only, so there is no hook tier to reach), or tma ships no install-hooks adapter for it and you would wire it by hand. When a daemon is running, the reason picks up ; a daemon is running and provides fallback capture (tier 3).

Three other continuation lines show up when they apply. demoted: is the interesting one: the pane registered through a hook, but its current state came from capture, because output kept arriving that its hooks did not account for. That is a suspect-wiring signal, not a proof: the usual cause is an agent restarted without the wiring or a missing wrapper, so run tma install-hooks --check. A hook claiming working accounts for the pane’s output until capture contradicts it, so a long tool call does not demote a healthy pane. model: names the model the pane stamped, and adds unrecognized: no [telemetry.windows] entry names it only for a pane whose context channel would have to size its gauge from that table. No shipped channel does, so on a normal install that line is the model name and nothing else. api: flags a pending permission request with no reachable endpoint.

A pane that is not listed at all

If the pane you care about has no line under panes, doctor has already told you one of three things somewhere above (it is behind a remote shell, it carries @agent_ignore, or the procs: line says the process walk failed) or the answer is that identity did not resolve. tma debug explain prints the whole decision for one pane:

$ tma debug explain %0
pane      %0  (work:0.0)
command   zsh
title     dev-box
flags     alternate_on=false scrolled=false history_view=false window_activity=1786903086
agent     (none — no manifest process_names matched)
process   1 procs in pane tree

The agent (none — …) line is the verdict, and it stops there: with no agent identified there is nothing to fold. The three usual causes are all readable from those lines. The pane’s foreground is a shell and the agent is not running in it. The agent’s process name is spelled differently from every manifest’s process_names (compare against the process count and check the manifest). Or the pane carries @agent_ignore, which explain names directly.

On a pane that did resolve, the same command keeps going: it prints the prior stamp, the evidence records, every screen rule with a [match] or [ - ] marker beside it, and the verdict with the winning evidence source. That is the tool for “detected, but as the wrong state” as opposed to “not detected”. See The detection model.

Remote and ignored panes

Both are reported, and neither is a warning.

remote:  1 pane(s) behind a remote shell: an agent there reports only if it can reach this tmux socket (see docs/how-to/agents-in-containers.md)
  - %10 work:4.0 (ssh)

A pane whose foreground is ssh, mosh, docker, podman, or kubectl is out of scope by classification: neither the process walk nor a capture crosses that boundary. Running an agent elsewhere is a choice, not a misconfiguration, so doctor names it rather than complaining. A pane that still carries stamps from before the boundary went up gets ; its @agent_* options are held, not refreshed appended, which is the honest description: nothing is updating them. To make an agent behind one of those actually report, give its hooks a route back to this socket, which is Run an agent in a container.

ignored: 1 pane(s) excluded from detection: unset the option to bring one back (`tmux set-option -pu -t <pane> @agent_ignore`)
  - %1 work:1.0 (ignored via @agent_ignore = manual)

@agent_ignore is your own opt-out, so doctor shows the value you set and the command that undoes it. Nothing else about that pane is evaluated.

A third section, nested:, lists panes running another multiplexer client. Agent state lives on the inner server, so run tma there.

Gate CI on the report

--exit-code turns the findings into a build failure, which is what a dotfiles job wants: it catches hook drift a config change introduced, not just an absent install.

tma install-hooks claude --yes
tma doctor --exit-code || exit 1

The verdict goes to stderr, so --json on stdout stays parseable:

tma: doctor: 2 warning(s), 1 pane(s) below the tier their manifest supports

Counted as a warning: a missing wrapper, each tmux hook that is not present, each skipped manifest and each action naming an unknown agent, each unreachable process_names entry, each stamp tma cannot decode or did not write, a detached server with no daemon, status off, mouse bindings without mouse on, a failed notify command, and per pane, incomplete hook wiring, a hook demotion, and a pending permission with no endpoint.

Deliberately not counted: a daemon that is not running, a daemon version skew, no ambient poll, no watcher, and the nested/remote/ignored sections. Those are runtime choices, so a wired agent sitting at tier 2 gates green.

The second number counts panes below the tier their manifest supports, which is 2 for an agent tma can wire and 1 for a hookless or adapter-less one. An unwired hook-capable agent counts; a wired one at tier 2 for want of a daemon does not.

Note that a detached scratch server counts as a warning unless a daemon is running, since nothing there would drive the polling floor. When the CI server has no agent panes to diagnose, tma install-hooks --check is the narrower gate over the wiring alone, with the same 0/1 contract.