Skip to content

pr3m/claude-code-warden

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

4 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸ›‘ claude-code-warden

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.


Install

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


What you get

Tab states

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.

The cockpit

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

Renaming a tab

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


How it works

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 ; … BEL for the tab; OSC 1 is ignored on Ghostty (ghostty#1026).
  • The spinner reads a cheap per-session render file each frame (no jq in 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. PreToolUse and PostToolUse touch a per-session .beat file; PreToolUse also drops an .inflight marker that PostToolUse removes. 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.


Configuration

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

Extending warden

warden's core stays universal. Your personal, cross-project behavior plugs in at the user level via three layers β€” never by forking the plugin:

  1. Config β€” the table above (glyphs, frames, thresholds).
  2. Drop-in extension scripts at ~/.claude/warden/ext/ (see examples/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 read wunda Β· 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.
  3. The status bus β€” ~/.claude/warden/sessions/<id>.json is 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 support

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.


Troubleshooting

  • 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: set CLAUDE_CODE_DISABLE_TERMINAL_TITLE=1 (see Install β€” the env block in ~/.claude/settings.json) and restart your sessions. /warden:doctor reports 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 on to ~/.tmux.conf.
  • tty resolved : FAILED β€” warden couldn't find a real PTY; titles won't paint. Run /warden:doctor for details.

Roadmap

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

Built by

Christjan Schumann β€” also claude-code-roam and claude-code-bash-smart-approve.

MIT licensed. PRs welcome β€” especially terminal adapters.

About

No description, website, or topics provided.

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages