Keep watch over a fleet of parallel Claude Code sessions β right from your terminal tabs.
You run Claude Code in a dozen tabs. Which ones are working? Which finished? Which is quietly blocked waiting for your permission while you stare at a different tab? warden answers all three at a glance, without you switching tabs:
β π§ wunda β spinning: working, running a bash command
β Ή π§ͺ redmy β spinning: working, running tests
β wundamental-web β needs you (permission / input) β escalates if ignored
π’ β Ό π€ mia β nothing has happened for a while β worth a look
β
smartbeat β done, your move
Β· personal β idle
An animated spinner rides each tab while Claude works, a glyph tells you what
kind of work, a needs-you alarm escalates so nothing waits unnoticed, and
/warden:cockpit gives you the whole fleet in one view. Be in the pilot seat.
Ghostty-first β but the core is standard terminal escape sequences, so it works in iTerm2, Terminal.app, and tmux too.
/plugin marketplace add pr3m/claude-code-warden
/plugin install warden
One required step: Claude Code writes its own terminal title (Β· working,
β³ idle) and will overwrite warden's. Turn it off in ~/.claude/settings.json
(top level), then restart your sessions:
"env": {
"CLAUDE_CODE_DISABLE_TERMINAL_TITLE": "1"
}(Official env var, confirmed by Anthropic.
Without it the spinner still shows while a turn runs, but warden's label/β
vanishes the instant the turn ends and Claude repaints the title.)
Otherwise no config: warden is hook-driven and starts on your next prompt in any
tab. (jq recommended: brew install jq β the spinner and glyphs work without
it; the context meter and cockpit need it.)
Verify: /warden:doctor (it reports whether the title override is disabled).
| Glyph | State | When |
|---|---|---|
β (animated) |
working | Claude is on your turn |
π§ π§ͺ π§Ή π βοΈ π π π€ ποΈ β‘ π π§ |
activity | what kind of work, from the tool in flight (π§ = thinking, no tool running) |
β |
needs you | Claude asked for permission/input |
βΌοΈ |
escalated | still waiting after escalateAfterSeconds (+ re-ping) |
π’ |
stalled | nothing has happened for stuckAfterSeconds, and no tool is running |
β³ |
stalled longer | same, past stuck2AfterSeconds β or a single tool has run for slowToolAfterSeconds (a command hung on stdin looks exactly like this) |
β
|
done | turn finished β your move |
Β· |
idle | session open, nothing running |
π’ measures progress, not duration. An agent may legitimately work for
hours; what matters is whether anything is still happening. warden beats a
heartbeat on every tool call and every tool return, and a tool that is genuinely
executing suppresses the stall markers β so a 20-minute test suite spins happily,
while a session that went quiet five minutes ago flags itself.
Each tab also shows the project (git repo name by default) and, past a
threshold, the context-window fill (Β·78%) so auto-compaction never
surprises you. The meter is recomputed during the turn, so it warns you on the
turn that crosses the threshold rather than after it.
/warden:cockpit
π‘ warden Β· 2 working Β· 1 need you Β· 1 stalled Β· 1 done Β· 1 idle
βΌοΈ escalated wundamental-web 4m what should the CTA say?
β needs_you mia 2m approve the migration?
π’ stalled sage π 6m 41% trace the booking webhook
β working wunda π§ 14s 78% fix the SOF marker map
β³ slow op redmy π§ͺ 22m add the retry tests
β
done smartbeat - reconcile the LHV export
Β· idle personal -
Rows are ranked by what needs a human first: escalated, then blocked, then stalled, then healthy work. A stalled row's clock counts since the last sign of life, not since the turn began. The cockpit derives that independently of the spinner, so a session whose animator died still reports the truth rather than cheerfully claiming to be working.
For a live view that redraws every 2s, run ~/.claude/warden/bin/warden cockpit
in its own Ghostty split.
warden owns the tab title so it can animate the spinner β which means a native terminal rename gets overwritten on the next tick. Set a custom label that warden keeps decorating with the live glyph instead:
/warden:label WUNDA-627 portal # or: warden label "WUNDA-627 portal"
β the tab reads β π§ WUNDA-627 portal while working, β
WUNDA-627 portal when
done. The label persists across turns and session resume, per tab.
warden label # show the current label
warden label --clear # back to the auto label (git repo / dir name)
WARDEN_LABEL="My tab" claude # name it at launch
Precedence: an explicit label β $WARDEN_LABEL β the auto label. To rename a
background session you see in the cockpit: warden label --session <id> "Name".
warden is pure Claude Code hooks + standard terminal escape sequences. No background services beyond a tiny per-session spinner while a turn is active.
UserPromptSubmit ββΊ working ββΊ start spinner daemon ββ
PreToolUse ββΊ activity glyph Β· beat Β· tool in flight
PostToolUse ββΊ π§ thinking Β· beat Β· flight cleared
Notification ββΊ β needs you ββΊ start escalation timer
Stop ββΊ β
done ββΊ stop daemons
β
each transition βββΌβββΊ STATUS BUS ~/.claude/warden/sessions/<id>.json
ββββΊ OSC 0 title ββΊ your terminal tab
- OSC 0, not OSC 1 β Ghostty (and others) honor
ESC ] 0 ; β¦ BELfor the tab; OSC 1 is ignored on Ghostty (ghostty#1026). - The spinner reads a cheap per-session render file each frame (no
jqin the hot loop) and resolves the terminal's real device (/dev/ttysNNN) so it can paint a tab even when that tab isn't focused. - Progress is evidence, not a guess.
PreToolUseandPostToolUsetouch a per-session.beatfile;PreToolUsealso drops an.inflightmarker thatPostToolUseremoves. Stall detection reads those two mtimes and nothing else β turn duration is never an input. - The spinner daemon owns everything that needs a clock (stall detection, the context meter), because a hook runs on the critical path of a tool call and has a 5-second timeout. It also self-heals: if the animator dies mid-turn, the next tool call revives it.
- On Ghostty/WezTerm it also emits an OSC 9;4 native progress pulse on the focused tab; a no-op elsewhere.
With "spinner": false there is no daemon, and therefore no stall detection and
no mid-turn context refresh β the tab still shows state, activity, and label.
~/.claude/warden/config.json (created on first run). /warden:config to view.
| Key | Default | Meaning |
|---|---|---|
spinner |
true |
animate the tab while working |
spinnerFrames |
braille | array of frames β try ["π","π","π","π","π","π","π","π"] |
spinnerIntervalMs |
120 |
frame interval |
showProject / showActivity / showContext |
true |
what rides the tab |
escalateAfterSeconds |
45 |
needs-you β escalated threshold (0 = off) |
escalateReping |
true |
re-ping the system sound on escalation |
escalateMaxSeconds |
3600 |
stop nagging a session nobody ever came back to |
stuckAfterSeconds / stuck2AfterSeconds |
300 / 900 |
π’ / β³ thresholds β seconds since the last tool call or return, not since the turn began |
slowToolAfterSeconds |
900 |
how long one tool may run before β³ (catches a command hung on stdin) |
contextWarnPercent |
75 |
only show the context meter past this |
contextRefreshSeconds |
15 |
how often the daemon recomputes the context meter mid-turn |
maxLifetimeSeconds |
86400 |
backstop for a session that crashed without emitting Stop β not a turn limit |
glyphs.* |
see above | override any state glyph |
projectLabelCommand |
β | a shell command ($WARDEN_CWD) printing a label |
warden's core stays universal. Your personal, cross-project behavior plugs in at the user level via three layers β never by forking the plugin:
- Config β the table above (glyphs, frames, thresholds).
- Drop-in extension scripts at
~/.claude/warden/ext/(seeexamples/ext/):project-label.shβ gets the cwd as$1, prints a custom tab label. Use it to map a repo to its active ticket from a second brain, so a tab can readwunda Β· WUNDA-627. It's global, so one script serves every project.on-state.shβ gets the session JSON on stdin on every transition (detached, non-blocking). React however you like: log it, notify, append to a tracker.
- The status bus β
~/.claude/warden/sessions/<id>.jsonis a documented contract. Because Claude Code merges hooks across plugins, a separate plugin can subscribe to the same events and read the bus β fully decoupled.
Status bus schema:
{ "id": "β¦", "state": "working|needs_you|done|idle",
"attention": "|stalled|stalled2|slow_tool|escalated",
"project": "wunda", "activity": "π§", "tty": "/dev/ttys003",
"cwd": "β¦", "started": "1719582000", "prompt": "fix the β¦",
"ctx": "78", "needs_since": "", "transcript": "β¦", "updated": "1719582012" }state is what the session is doing; attention is whether it needs you. They
are orthogonal β a working session can be stalled, and a needs_you session
can be escalated. Both fire on-state.sh, so an extension sees a stalled
session, not just whoever happens to be looking at that tab.
| Terminal | Tab spinner + glyphs (OSC 0) | Progress pulse (OSC 9;4) | Adapter extras |
|---|---|---|---|
| Ghostty β₯ 1.2 | β | β | β |
| WezTerm | β | β | β |
| iTerm2 | β | β | tab color / badge / jump-to-tab (roadmap) |
| Terminal.app | β | β | β |
| VS Code terminal | β | β | β |
| tmux | β ΒΉ | β ΒΉ | β |
ΒΉ tmux needs set -g allow-passthrough on.
- warden's label/glyph shows while working but reverts when the turn ends β
this is the big one. Claude Code writes its own title (
Β·working,β³idle); it overwrites warden's the moment a turn finishes. Fix: setCLAUDE_CODE_DISABLE_TERMINAL_TITLE=1(see Install β theenvblock in~/.claude/settings.json) and restart your sessions./warden:doctorreports whether it's disabled. (Claude's title write is hardcoded and event-driven, so warden can't beat it from a hook β it has to be turned off.) - Nothing appears in Ghostty β make sure you don't have
title = β¦pinned in your Ghostty config (it freezes escape-sequence titles). Run/warden:doctor. - In tmux β add
set -g allow-passthrough onto~/.tmux.conf. tty resolved : FAILEDβ warden couldn't find a real PTY; titles won't paint. Run/warden:doctorfor details.
- Phase 2 β interactive cockpit TUI: cursor through sessions, jump to a tab, per-session history/timeline, all on the same status bus.
- iTerm2 adapter: native tab background-color flash on needs-you, a "needs input" badge, and true auto-focus/jump via the iTerm2 Python API.
- Menubar fleet indicator (SwiftBar/xbar):
π’2 π‘1 π΄1, always visible. - kitty / WezTerm adapters, configurable sound packs, done-with-summary tab.
Christjan Schumann β also claude-code-roam and claude-code-bash-smart-approve.
MIT licensed. PRs welcome β especially terminal adapters.