Install
openclaw skills install @dandriscoll/muxrayInspect tmux panes as JSON: snapshot, diff, and classify Claude/Codex/Copilot agent state across the whole fleet.
openclaw skills install @dandriscoll/muxraymuxray turns a live tmux pane into deterministic JSON so you can read what a
program is doing without scraping raw terminal bytes. Reach for it when you are
supervising one or more interactive CLIs in tmux — especially terminal
coding agents (Claude Code, Codex, Copilot) — and you need to know what state a
pane is in, what changed, or whose turn it is.
muxray update.tmux skill.running, needs_approval, waiting_for_input, error, or completed.tmux skill
(tmux send-keys). muxray only reads.tmux capture-pane -p is
simpler. muxray's value is the classification and diff, not raw bytes.Prefer the bundled wrapper for any call whose pane/session target comes from context you do not fully control — it validates the target and restricts you to the fixed read-only subcommands, so no untrusted text reaches a shell:
{baseDir}/scripts/muxray-run.sh status --pane work:1.0
{baseDir}/scripts/muxray-run.sh scan --text
For a fully-supervised, literal target you may call muxray directly. Either
way: fixed subcommand, explicit --pane/flags, never an interpolated shell
string. muxray takes no shell input and parses no expressions.
| Command | What you get |
|---|---|
muxray list | every tmux session/window/pane, structured |
muxray status --pane <t> | the program + state classified for that pane |
muxray scan | every pane classified in one call (the fleet view) |
muxray watch --pane <t> [--until <states>] | block until the pane settles |
muxray snapshot --pane <t> [--out <f>] | capture the pane (stored locally) |
muxray diff --pane <t> [--since <f|id>] | what changed vs a previous snapshot |
muxray inspect --pane <t> | snapshot + diff + status in one call |
muxray doctor | environment check (tmux present, store writable) |
--text gives a terse human line instead of JSON. muxray <cmd> -h lists a
command's flags.
Pane targets (--pane): session · session:window.pane (work:1.0) ·
pane id (%3) · session id ($0) · omitted = the current pane when inside
tmux. --session <name> is a clearer equivalent for whole-session targeting.
Every result carries an envelope: schema_version (currently "2"),
command, muxray_version, generated_at. Branch on schema_version — if
it is not the version you coded against, the shape may have changed.
status and inspect carry a classification:
{ "program": "codex", "status": "running", "rule_id": "codex.running",
"confidence": 0.88, "evidence": "Working (esc to interrupt)" }
program — claude, codex, copilot, shell (an interactive shell
prompt — the harness is not live; reported as idle), or unknown (a pane
it does not recognize: editor, pager, transcript, muxray's own output).status — one of: idle, running, blocked, waiting_for_input,
needs_approval, error, completed, unknown.program=unknown — "parse
it yourself," not "something failed." A footer that is a shell prompt is
program=shell/status=idle: a dropped connection or exited agent is not
an agent error.--explain to attach a trace of every rule considered — use it to
diagnose an unknown.diff carries changed (bool — both true and false are exit 0; change
is not an error), summary, added/removed/context line arrays, hunks,
and the previous_snapshot/current_snapshot ids.
muxray snapshot --pane work:1.0 --out before.json # capture a baseline
# ... let the program work ...
muxray diff --pane work:1.0 --since before.json # what changed
--since also accepts a snapshot id, or is omitted/latest to diff against
the most recent stored snapshot of that pane (muxray keeps a local store, so you
often don't need --out at all). changed: false is deterministic and
reproducible across machines — the hash is over cleaned text only.
Interpreting a diff: read summary first, then added (new output the
program produced) vs removed (lines that scrolled off or were replaced).
hunks counts distinct change regions. A spinner/elapsed-timer line flipping
between captures shows up as a tiny diff — treat a 1-line cosmetic change as
"still working," not "made progress."
Two verbs are the loop — you don't hand-roll poll+sleep+compare:
muxray watch --pane <t> blocks until the
pane stops working, then prints the final classification and exits 0.
--until = any settled state (idle, completed,
needs_approval, waiting_for_input, blocked, error); it waits
through running and transient unknown.--until idle,needs_approval.--timeout 5m exits 5 if it never settles (the last-seen
classification is still emitted).status: needs_approval/waiting_for_input →
hand off to a human; error → restart/alert; idle/completed → assign
the next task (if program=shell, the pane dropped to a shell — relaunch,
don't assign to a live agent).muxray scan classifies every pane in one call →
{ "panes": [ { "target": "%3", "session": …, "classification": {…} } … ] }.
target is the pane id (%N) — feed it straight back into
status/watch/diff. A pane that can't be read is reported unknown with
an error class rather than failing the whole scan.The wrapper {baseDir}/scripts/muxray-watch-diff.sh runs the canonical
"baseline → wait until settled → diff → classify" sequence for one pane and
prints all three results — use it to summarize a single agent's working turn.
To answer "what is this Claude/Codex/Copilot pane doing?":
muxray status --pane <t> → the classification is the answer: program
names the agent, status names its state, evidence is the footer line that
decided it.muxray inspect --pane <t> adds the
snapshot and a diff against the last baseline in one call. Read tail_excerpt
/ the diff added lines for the most recent output.needs_approval / waiting_for_input mean the agent is paused on a human —
surface the prompt and stop; do not auto-approve.unknown with a recognizable agent in scrollback usually means the live
frame scrolled away or the pane is mid-redraw. Re-status once; if still
unknown, fall back to reading tail_excerpt yourself.work:1.0 — Codex, needs_approval (asking to run rm -rf build/)."diff.summary + the few added lines that matter;
do not paste the whole pane.muxray scan --text (one line per pane) and call out only
the panes that need action..env echoes). muxray does not
redact snapshot.raw/clean, diff lines, or tail_excerpt. You must
summarize or redact obvious secrets before showing output to the user or
sending it anywhere external. Prefer reporting classification fields over raw
pane dumps; pass --no-raw to drop the raw capture from a snapshot.muxray update (downloads a verified release; sends nothing). Do not run
update, telemetry, or bundle --include-excerpt as part of an
observation loop — they are operator actions, not agent steps.0 ok (incl. changed:true/false) · 1 internal · 2 usage · 3
tmux/capture · 4 snapshot not found · 5 watch timed out. On failure stderr
carries error.class (a stable, branchable id) and error.hint (the next
action).
references/json-contract.md — compact field/state cheat-sheet.examples/inspect-agent.md — a worked end-to-end example.muxray usage — the full in-binary calling contract.