Knowing which Claude is stuck
10 July 26
I keep one zellij session per project, and most of them have a Claude Code agent running inside. Four or five at once is normal. The agents work for minutes at a stretch, so I spend the day asking which one has stopped and is waiting on me.
Zellij’s built-in session manager, which I had bound to Alt s, lists the
sessions without saying anything about what is inside them. Answering the
question meant attaching to each session in turn and looking. What I wanted was
a picker over the live sessions only, ordered most-recently-used first. That
ordering makes Alt s then Enter a return to the previous session, the way
cmd-tab does. I also wanted fuzzy matching over the names, and an indicator on
each row showing how many agents were running and what they were doing.
The obvious way to get that last part is also the wrong one. Claude Code continuously sets the terminal title via an OSC escape, which zellij surfaces as the pane title. You can read another session’s pane titles from outside it:
$ zellij --session panic_tda action list-panes -c -j | jq -r '.[].title'
✳ Run balanced_panel_5x5 experiment and verify T2I batching
That ✳ is one frame of a spinner that changes about ten times a second. The
whole string belongs to Claude Code’s UI, which can be redesigned any Tuesday,
and parsing that is a mug’s game (and slow). Instead, we can get the state from
somewhere better. Claude Code fires
hooks at exactly the points
in its lifecycle where the state changes. I already had a script on Stop,
sending me a push notification whenever a long turn finished. Widening it was
easy: SessionStart writes idle, UserPromptSubmit writes working, Stop
writes idle again, Notification writes blocked, and SessionEnd removes
the file. Each agent gets one small file, under
$XDG_RUNTIME_DIR/claude-agent-state/<zellij-session>/<pid>.1 The agent
reports what it is doing, so nothing has to infer it.
Counting the agents is a separate problem, but every zellij session runs a
server whose command line ends in the session name. Each claude process chains
up through its pane’s shell to exactly one of them:
$ ps -eo pid=,ppid=,args= | grep -E '[z]ellij --server'
751042 1 .../zellij --server /run/user/1001/zellij/contract_version_1/panic_tda
2863588 1 .../zellij --server /run/user/1001/zellij/contract_version_1/comp4020
A single ps, plus a walk up the parent chain, gives an authoritative count of
who is alive. The state files supply the status. Keeping the two apart matters,
because an agent killed outright never fires SessionEnd, leaving its file
behind. The switcher therefore drops any file whose pid is gone. The whole thing
runs in about 50ms, making no IPC call to any other session.
Here is what Alt s puts on screen:
switch to > < 4/4 ─────────────────────────────────────────────────────────
⏳ working ⚠ blocked ✓ idle · unknown
▌ comp4020 2 ⚠1 ⏳1
▌ dotfiles 1 ⏳1
▌ panic_tda 2 ⏳1 ✓1
▌ slop-salon 1 ✓1
comp4020 has two agents, one of them waiting on me, so that is where I go
next. The first row is always the session I was in before this one. Alt s and
Enter is therefore a two-keystroke toggle between the last two projects. The
built-in session manager, which I still want for creating sessions and
resurrecting dead ones, keeps its default binding on Ctrl o then w.
A zellij pane rename is sticky: it overrides every subsequent OSC title until
you undo it. My first instinct was to stamp the state into the pane name
permanently. That throws away Claude Code’s own title, which while an agent is
working tells me more than any glyph I could substitute. The rename now happens
only on Notification, setting the pane name to ⚠ and whatever Claude says it
wants. The next UserPromptSubmit calls undo-rename-pane. Zellij tracks the
program’s title underneath the manual name throughout, so the undo restores the
current one rather than the stale one.
The bug worth confessing is that I first keyed those state files by Claude’s
session id rather than the pid. The id changes when a session is resumed or
compacted; the process does not. A single agent therefore accumulated two files,
and the switcher rendered dotfiles 1 ⏳2, which is one agent reporting two
states at once. One agent is one process, so the file is named for the pid now.
I only noticed because I took the screenshot above for this post.
Everything is in my dotfiles if you want to reconstruct it:
bin/zj-switch
is the switcher,
bin/claude-turn-tracker
writes the state, the Alt s binding is in
zellij/config.kdl
and the hooks are in
claude/settings.json.
You need fzf and zellij 0.44 or newer, for
switch-session and for rename-pane --pane-id.
Footnotes#
-
If you go poking at this, note that zellij also uses
XDG_RUNTIME_DIRto find its sockets. I spent a while convincedrename-panewas broken. In fact I had overridden that variable to sandbox the state files, blinding the zellij CLI in the process. ↩
Cite this post
@online{swift2026knowingWhichClaudeIsStuck,
author = {Ben Swift},
title = {Knowing which Claude is stuck},
url = {https://benswift.me/blog/2026/07/10/knowing-which-claude-is-stuck/},
year = {2026},
month = {07},
note = {AT-URI: at://did:plc:tevykrhi4kibtsipzci76d76/site.standard.document/2026-07-10-knowing-which-claude-is-stuck},
}