See what every Claude Code session is doing. Each iTerm2 tab shows a status prefix. β‘ running, π€ idle, or π΄ needs attention (with flashing).
In Claude Code, register the marketplace first:
/plugin marketplace add JasperSui/jaspersui-marketplaceThen install the plugin from this marketplace:
/plugin install iterm2-tab-status@jaspersui-marketplaceOn first session start, the plugin automatically:
- Creates an iTerm2 Python runtime (if not already installed)
- Deploys the tab-status adapter and COS overlay scripts to iTerm2 AutoLaunch
- Deploys COS readback and safe-dispatch scripts to the iTerm2 Scripts menu
After the first session, restart iTerm2 (or toggle Scripts β AutoLaunch for claude_tab_status.py and cos_iterm_overlay.py).
If auto-bootstrap didn't work, run:
/iterm2-tab-status:setup
Run in Claude Code:
/iterm2-tab-status:uninstall
Then remove the plugin:
claude plugin uninstall iterm2-tab-status| State | Prefix | Tab Color | Badge | Dismiss on Focus |
|---|---|---|---|---|
| Running β Claude is processing | β‘ | No change | No | No |
| Idle β Claude finished | π€ | No change | No | No |
| Attention β needs permission | π΄ | Flashes orange | Yes | Yes |
Lifecycle: User submits β β‘ β Claude finishes β π€ β User submits β β‘ β Claude needs permission β π΄ flash! β User focuses β cleared
Your original tab color, title, and badge are saved and restored.
Claude Code hooks β JSON signal file β iTerm2 adapter β tab status
No screen scraping. Claude Code's official hooks API writes a signal file on every event. The unified hook handles both UserPromptSubmit (β running) and Notification (β idle/attention). The iTerm2 adapter polls for signal files and sets the matching tab's prefix, color, and badge by TTY. Only the attention state flashes and shows a badge β running and idle are informational prefixes that persist.
The easiest way to configure is with the slash command in Claude Code:
/iterm2-tab-status:config
This opens an interactive prompt to change flash color, prefixes, badge, notifications, and more.
Settings are stored in ~/.config/claude-tab-status/config.json. Example with all keys and their defaults:
{
"dir": "~/.cache/claude-tab-status",
"color_r": 255,
"color_g": 140,
"color_b": 0,
"interval": 0.6,
"prefix_running": "β‘ ",
"prefix_idle": "π€ ",
"prefix_attention": "π΄ ",
"display_target": "title",
"subtitle_activity_source": "off",
"badge": "β οΈ Needs input",
"badge_enabled": true,
"notify": false,
"sound": ""
}The config file is hot-reloaded β changes take effect within ~1 second, no restart needed.
Settings are resolved in this order (highest wins):
- Environment variable (e.g.
export CLAUDE_ITERM2_TAB_STATUS_COLOR_R=255) - Config file (
~/.config/claude-tab-status/config.json) - Built-in defaults
Environment variables are useful for CI or per-machine overrides without touching the config file.
By default, status is shown as a tab title prefix.
Set "display_target": "subtitle" to leave the main tab title alone and write status to the iTerm2 user variable user.claudeStatus. In iTerm2, open Settings > Profiles > General and set Subtitle to:
\(user.claudeStatus)
Use "display_target": "both" to update both the title prefix and subtitle variable.
Set "subtitle_activity_source": "prompt" to append a compact, sanitized activity snippet
to the subtitle, such as β‘ Run tests. The default is "off", which keeps subtitle
output status-only and does not persist prompt text in signal files. Prompt snippets are
opt-in because Claude Code's UserPromptSubmit hook payload includes the submitted
prompt.
Claude Code can also set terminal titles. If you want iTerm2 to control the main title while this plugin updates the subtitle, add this to your shell startup file:
export CLAUDE_CODE_DISABLE_TERMINAL_TITLE=1Environment variable reference
| Variable | Default | Description |
|---|---|---|
CLAUDE_ITERM2_TAB_STATUS_DIR |
$XDG_RUNTIME_DIR/claude-tab-status or ~/.cache/claude-tab-status |
Signal file directory (per-user, mode 0700) |
CLAUDE_ITERM2_TAB_STATUS_COLOR_R |
255 |
Flash color red (0-255) |
CLAUDE_ITERM2_TAB_STATUS_COLOR_G |
140 |
Flash color green (0-255) |
CLAUDE_ITERM2_TAB_STATUS_COLOR_B |
0 |
Flash color blue (0-255) |
CLAUDE_ITERM2_TAB_STATUS_INTERVAL |
0.6 |
Flash interval in seconds |
CLAUDE_ITERM2_TAB_STATUS_PREFIX_RUNNING |
β‘ |
Running state prefix |
CLAUDE_ITERM2_TAB_STATUS_PREFIX_IDLE |
π€ |
Idle state prefix |
CLAUDE_ITERM2_TAB_STATUS_PREFIX_ATTENTION |
π΄ |
Attention state prefix |
CLAUDE_ITERM2_TAB_STATUS_DISPLAY_TARGET |
title |
Where to show status: title, subtitle, or both |
CLAUDE_ITERM2_TAB_STATUS_SUBTITLE_ACTIVITY_SOURCE |
off |
Subtitle activity source: off or prompt |
CLAUDE_ITERM2_TAB_STATUS_BADGE |
β οΈ Needs input |
Badge text (attention only) |
CLAUDE_ITERM2_TAB_STATUS_BADGE_ENABLED |
true |
Enable/disable badge (attention only) |
CLAUDE_ITERM2_TAB_STATUS_NOTIFY |
false |
macOS notification (attention only) |
CLAUDE_ITERM2_TAB_STATUS_SOUND |
(empty) | Sound file path (attention only) |
CLAUDE_ITERM2_TAB_STATUS_LOG |
WARNING |
Log level (DEBUG, INFO, WARNING, ERROR) |
Tab doesn't show status β Check that the iTerm2 Python Runtime is installed. Verify signal files are created: ls "${XDG_RUNTIME_DIR:-$HOME/.cache}/claude-tab-status/" after Claude goes idle. Set export CLAUDE_ITERM2_TAB_STATUS_LOG=DEBUG and check iTerm2's script console (Scripts β Manage β Console).
Wrong tab gets prefix β The TTY in the signal file doesn't match the iTerm2 session. Restart iTerm2.
The tab-status signal directory is also the safest integration point for a chief-of-staff tab. Do not use iTerm coprocesses for normal COS monitoring: coprocess stdout is typed back into the terminal session, which is too risky for worker orchestration.
Use the read-only monitor instead:
python3 scripts/cos_tab_state_monitor.py --printIt reads ${XDG_RUNTIME_DIR:-$HOME/.cache}/claude-tab-status/*.json, dedupes
stale Codex rollout files by live TTY/PID, and writes:
~/.claude/plans/fleet-reports/tab-state-current.json~/.claude/plans/fleet-reports/tab-state-events.jsonl
The bootstrap installs scripts/cos_iterm_overlay.py into iTerm2 AutoLaunch.
It polls tab-state-current.json every COS_ITERM_OVERLAY_INTERVAL seconds
(default: 2.0) and mirrors state into iTerm2 user variables:
user.cosRoleuser.workerStateuser.workerReadinessuser.workerGoaluser.lastFleetReportuser.workerRuntimeuser.workerCwd
Set COS_TTYS=/dev/ttys006 to mark the COS tab explicitly. COS identity is
explicit only; tabs are not guessed to be COS from their working directory.
The bootstrap also installs these iTerm2 API scripts into the regular Scripts directory:
scripts/cos_iterm_daemon.pyis the preferred COS iTerm2 daemon. It runs inside iTerm2's Python API runtime, observes sessions without focusing tabs or sending input, writes~/.claude/plans/fleet-reports/iterm-live-state.json, appendsiterm-live-events.jsonl, classifies readiness from prompt/screen state, and sets theuser.*variables above for status bars/subtitles.scripts/cos_iterm_edge_daemon.pyis the authoritative C2 tab edge. It runs inside iTerm2's Python API runtime and exposes a same-user mode-0600 Unix socket at~/.cache/cos-c2/iterm-edge.sock. The daemon resolves the exact registered iTerm session UUID, verifies the expected live coord-api lease epoch immediately before injection, submits prompt + CR + LF through the iTerm API, and returns an acknowledgment/receipt. Control Room and MCP wrappers use this same adapter contract; AppleScript is fallback only.scripts/cos_iterm_readback.pyprints live iTerm2 session variables as JSON. Use it to prove the AutoLaunch daemon/overlay is loaded and setting variables.scripts/cos_tab_dispatch.pyvalidates complete registered-worker C2 envelopes. Authoritative dispatch uses the iTerm API edge, exact session UUID, controller epoch fencing, idempotency, and append-only receipts. Legacy TTY-only/goaldispatch remains available for compatibility but is not a C2 authority path.scripts/cos_dispatch_orchestrator.pyselects an eligible worker for a dry-run plan. Legacy/goal ...dispatch is intentionally dry-run only; live V1 dispatch must provide a complete envelope and manifest so the edge can reserve the worker, fence the controller epoch, and write a receipt.
Install and verify the iTerm API scripts directly:
python3 scripts/cos_iterm_api_install.pyDry-run a dispatch before sending:
python3 scripts/cos_tab_dispatch.py --dry-run --tty /dev/ttys003 --text '/goal inspect current task and report'
python3 scripts/cos_dispatch_orchestrator.py --dry-run --goal 'inspect current task and report' --cos-tty /dev/ttys006
# live dispatch (requires a validated envelope and the armed C2 edge)
python3 scripts/cos_dispatch_orchestrator.py --envelope /path/assignment.json --manifest /path/run-manifest.jsonBuild a COS dashboard from current tab signals and fleet reports:
python3 scripts/cos_tab_state_monitor.py --print
python3 scripts/cos_dashboard.pycosctl status is the pre-action read-only view for the bootstrap supervisor.
It includes the live lease, arm/readiness state, registered worker
classification, actionable coord feed, wake reasons, and a deterministic
decision digest. It does not reserve workers, dispatch prompts, or change
coord-api state.
The status fields distinguish a physical ARMED file from an effective arm:
armed reports that the marker exists, while arm_marker_valid and
effective_armed are only positive after the marker is validated against the
supplied manifest digest. A stale or malformed marker therefore remains visible
for diagnosis but cannot appear operationally armed; armed_but_invalid and
requires_explicit_rearm make the recovery action explicit. If no manifest is
provided, the effective value is null rather than an unverified claim.
Before a terminal experiment, run bash scripts/cosctl preflight --manifest <path>. It checks the exact manifest digest, plan paths, required launchd
registrations, and the iTerm edge health without enabling services or sending
input. A nonzero result is a hard stop for delivery. The JSON response includes
ordered blockers with stable codes (terminal_actions_disabled,
identity_drift, no_idle_registered_worker, edge_not_ready, and related
service/plan failures) plus bounded remediation text. The COS decision loop can
consume those codes directly; it must not infer permission to edit a manifest
or adopt a replacement identity from the diagnostic output.
When preflight reports identity drift, inspect the non-mutating proposal with
bash scripts/cosctl roster-proposal --manifest <path>. It compares expected
worker UUID/TTY/runtime bindings with live sessions and lists unregistered
sessions. It never edits the manifest; adoption requires an explicit re-arm.
When iterm-live-state.json exists, cos_dashboard.py prefers it over the
older signal-file snapshot so COS sees API-derived readiness (ready,
running, queued, needs_input, idle, unknown) without screen-scraping
from the conductor tab.
Watch fleet-report file drops/changes:
python3 scripts/cos_report_watcher.py --once --printRun the fast dry-run harness before touching live tabs:
python3 scripts/cos_dry_run_harness.pyCopy and fill the example manifest without storing credentials in it:
mkdir -p ~/.config/cos-c2
cp config/run-manifest.example.json ~/.config/cos-c2/run-manifest.jsonInstall the watchdog and iTerm API scripts separately. Both remain inert until the operator explicitly arms the supervisor:
python3 scripts/cos_iterm_api_install.py
bash launchd/install-cos-iterm-edge-launchd.sh \
--manifest ~/.config/cos-c2/run-manifest.json \
--state-dir ~/.local/state/cos-c2
bash launchd/install-cos-bootstrap-watchdog-launchd.sh
scripts/cosctl status
scripts/cosctl arm
scripts/cosctl run
scripts/cosctl poke
scripts/cosctl standby
scripts/cosctl stopThe API installer removes the legacy edge daemon from iTerm AutoLaunch; only
the observation daemon and overlay remain automatic iTerm scripts. The edge
LaunchAgent pins the selected manifest, state directory, socket, and
iTerm Python runtime. KeepAlive restarts the API transport after a crash with
launchd throttling. An advisory lock tied to the socket rejects any second edge
process before it can unlink or replace the live endpoint. The service does not
dispatch or interpret terminal state by itself.
The shared actuation lease and per-worker reservation still fence every input action.
arm is a deliberate unattended-work boundary; installation alone does not
arm anything. The actuation lease is
workspace:mikebook:c2-supervisor (180-second TTL, 60-second renewal), and a
30-second tick reconciles the registered fleet and writes a deterministic
decision. The launchd watchdog is the sole automatic wake actuator, preventing
the supervisor and watchdog from racing to inject the same decision.
The model publishes digest-bound program.md and current-focus.md recovery
projections in the state directory. <state-dir>/current-actions.txt remains a
compatibility name for the current-focus payload during migration. These are
UTF-8 Markdown with a versioned JSON header binding
the manifest, exact controller sessions, epoch, generation, decision digest,
previous action digest, status, durable references, and next-check deadline.
They contain bounded intent and coord-api identifiers, never a second task
database, authority grant, or raw untrusted message bodies. Publish a staged
current-focus update with:
scripts/cosctl checkpoint --from-file /path/to/staged-current-actions.txtOn wake, the edge injects only a fixed /goal C2_CONTINUE line naming the
absolute path and SHA-256. The model's first action is an exact progress ACK:
scripts/cosctl ack --digest <sha256> --generation <n> --epoch <n> \
--ownership visibleThe default next check is five minutes and every checkpoint must choose 60 to
1800 seconds. A changed deterministic decision wakes immediately even when the
declared deadline is later. Rewrite and checkpoint after every material worker,
message, PR, evidence, or blocker transition. Malformed or future-dated files
fail closed; complete additionally requires durable completion references and
a current deterministic decision with no pending wake.
The run manifest supports tab, headless, and ab for dispatch and recovery.
tab uses the iTerm2 Python API edge. headless resumes the same Codex/Claude
session UUID for one bounded turn and exits. ab selects deterministically and
records comparable latency, completion, duplicate, recovery, provider-failure,
and visible-reattachment metrics; neither transport is presumed superior.
The launchd watchdog is a 60-second health/recovery tick. Its installed plist
pins the selected manifest and state directory in ProgramArguments and writes
both output streams to <state-dir>/watchdog.log. It remains inert without the
state-local ARMED file, and it refuses all work when that marker is stale or
does not match the pinned manifest digest. A fresh process heartbeat is not sufficient health
when the current action generation is due or unacknowledged. The watchdog
distinguishes terminal injection, model acknowledgment, and a rewritten
checkpoint. It waits 90 seconds for the exact digest/generation/epoch ACK and
retries at most once. Even observed_ack=false enters this wait when terminal
bytes were attempted, so a false edge signal cannot cause an immediate
duplicate prompt.
After two expired ACK windows the watchdog writes a recovery hold. The visible
supervisor releases its epoch and cannot reacquire while held. Only after
coord-api proves that epoch absent may the watchdog run a bounded headless
resume of the same CLI UUID. A successful turn must acquire the successor
epoch, publish a new headless checkpoint with a fresh exact-digest receipt,
obtain and verify coord-api readback for that receipt, release it with
scripts/cosctl finish-turn --digest <sha256> --ownership headless, and exit.
Automatic epoch rebind is transport bookkeeping and never counts as model
progress. Repeated delivery of the same ACK also cannot refresh progress time.
Later deadlines may launch further bounded turns without waiting for the stale
visible TUI to mirror them. Explicit visible recovery uses
scripts/cosctl reattach --digest <sha256> after the headless epoch is absent.
Provider failures retain bounded backoff while lease and health checks continue.
While armed, the same tick also probes the edge socket with a two-second bound.
Health includes the SHA-256 of the exact manifest bytes loaded by the edge; the
watchdog compares it with the current on-disk manifest, so changing worker
registration or authority bounds forces a fenced edge reload even when the
human-readable manifest_id is unchanged. The edge performs the same comparison
before every dispatch, poke, or visual action and rejects input immediately on
drift; it never waits for the watchdog reload to fail closed.
One failed probe records degraded health without restarting anything; two
consecutive failures issue one scoped launchctl kickstart -k for
com.local.cos-iterm-edge and append an edge-recovery receipt. A successful
probe clears the failure counter, so a transient API hiccup cannot cause a
restart. If the edge remains unhealthy after recovery, additional restarts use
60/120/240/480/900-second exponential backoff while every 60-second probe still
runs; the first healthy probe clears the backoff immediately.
Do not run a live failure/A-B trial merely because the watchdog is armed. Safe preconditions are: an operator-approved failure injection, a preserved current manifest/state snapshot, confirmed coord lease/readback health, a verified tab-2-only target, no unrelated queued prompt, and an explicit rollback/visible reattachment plan. Installation or a healthy tick is not that authorization.
Interactive runtime state is a model decision, not a prompt-specific rule.
When terminal telemetry is blank, contradictory, or reports needs_input, C2
captures the registered tab and binds its screenshot digest, target identity,
timestamp, controller epoch, and existing worker-reservation epoch into a
VisualObservation. The supervising LLM
interprets that evidence and emits a bounded VisualDecision with its rationale.
Only then may the edge adapter reverify both leases and execute the requested
keypress or text through the visual_action operation. The adapter never
infers what a dialog means and never maps a
vendor string to an action. If visual capture or the supervising model is
unavailable, the worker remains fenced at needs_input unless the run manifest
contains a separately authorized fallback.
The bounded key surface includes Enter, Escape, Tab, and clear-line (Ctrl-U), plus printable text. Tab and clear-line are experimental recovery primitives: live Codex trials showed that a running session may require Tab to queue staged text, while a goal-blocked session may require clear-line before an exact resume command. These observations do not become adapter rules. Claude, Codex, and future runtimes use the same primitives, but the supervising LLM selects an action only from fresh visual evidence and the edge requires post-action visual verification. A successful key write alone is not message presentation, receipt, or execution evidence.
Visual decisions reserve their idempotency key durably before terminal input.
The append-only write result uses a child key and records only
key_write_succeeded; it keeps observed_ack and observed_presentation
false. A crash after injection therefore leaves a non-replayable reservation,
and concurrent executors serialize duplicate detection with the append. The
decision remains pending until a separately recorded fresh screenshot and
LLM verdict confirms the intended visual outcome.
After any visual action, capture again and require the LLM to confirm the
intended transition. A process title, blank screen API, successful write call,
or static isProcessing value is not acknowledgment. Headless completion also
marks the visible tab stale until a screenshot proves reattachment or a fresh
visible worker identity is registered.
Run the non-mutating COS control-plane daemon once:
python3 scripts/cos_control_daemon.py --once --printOptional launchd daemon:
bash launchd/install-cos-control-plane-launchd.sh
bash launchd/uninstall-cos-control-plane-launchd.shOptional helpers:
scripts/cos_tab_trigger_event.pyis a safe iTerm trigger target. Configure triggers to invoke it for lines likeDONE,BLOCKED,APPROVE,REJECT,Traceback,rate limit, ormerge conflict. Do not configure triggers to send text, inject data, or cancel commands automatically.
See CONTRIBUTING.md.
If this plugin saves you tab-switching time, consider giving it a β!

