I usually have several Claude Code sessions running side by side — a document here, a small fix there, some research. On a busy day, around five.
Delegating the work wasn't the problem. Noticing was:
- A session asking "may I run this?" gets buried behind other windows.
- A session finishes, and I don't notice for a while.
- I keep switching windows just to check.
The AI wasn't the slow part. The time until I noticed was. The more sessions I ran, the worse it got.
The setup: three small pieces
I put the state of each session on a small button box on my desk (a Stream Deck Neo: 8 keys and a thin info bar).
- Claude Code hooks run a small script at fixed moments — a prompt is submitted, a tool is about to run, permission is requested, the turn stops.
- The script writes one tiny JSON file per session: which folder, which session, which state. No prompt text, no tool output.
- A Stream Deck plugin reads those files every second and colors the keys.
The hook side is mostly a mapping from hook events to four states:
// hook events -> key state
function stateFor(d) {
switch (d.hook_event_name) {
case "UserPromptSubmit":
case "PostToolUse":
return "running"; // blue
case "PreToolUse": // asking me a question also counts as waiting on me
return ["AskUserQuestion", "ExitPlanMode"].includes(d.tool_name) ? "ask" : "running";
case "PermissionRequest":
return "ask"; // red
case "Notification":
if (d.notification_type === "permission_prompt") return "ask";
if (d.notification_type === "idle_prompt") return "waiting";
return null;
case "Stop":
return "waiting"; // orange: done, please look
case "SessionStart":
return "idle"; // gray
default:
return null;
}
}
The colors:
| Color | Meaning |
|---|---|
| Blue | Working |
| Red | Waiting for my permission |
| Orange | Done — please look |
| Gray | Idle |
Pressing a key brings that session's window to the front. See red, press it, approve in that window. No more hunting.
Where it broke
The bug that took longest: a session finished, but its key stayed blue.
The hook writes to a temp file and renames it over the real one. The plugin reads that file every second. On Windows, if the rename lands at the exact moment the plugin is reading, the rename can fail with EPERM. My first version silently swallowed that error — so the "done" state was never written, and the key stayed blue. By the time I found it, 28 orphaned temp files had piled up.
The fix is boring: retry a few times with a short wait.
function replaceFile(tmp, file) {
for (let i = 0; ; i++) {
try { fs.renameSync(tmp, file); return; } catch (e) {
if (i >= 9) { try { fs.unlinkSync(tmp); } catch {} throw e; }
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 30); // ~30 ms
}
}
}
Boring, but necessary. A status light you can't trust is just decoration.
Smaller one: after /clear, the session continues under a new ID, so I briefly had two keys with the same name. Now the hook removes the old record when a session starts from a clear, and keeps an old-ID → new-ID map for the plugin.
Then a character moved in
Colors alone felt a bit dry, so I drew a pixel-art chick into the keys. Her name is Aqui (from Aquila, the eagle constellation).
- Working: juggling a ball
- Waiting for permission: holding up a "?"
- Done: a "!"
- Idle: eyes closed, "z"
If the next key is also working, she passes the ball over. I wanted a box I like looking at, not just another notification.
That turned into a series of YouTube Shorts — over 20 episodes now. These days she rides around in a pixel-art WRX S4 through the seasons.
- Shorts playlist: https://www.youtube.com/playlist?list=PLOctjyutDnug
- How it works (2:25, Japanese with visuals): https://www.youtube.com/watch?v=m7cUQ3Owqv0
If you only do one thing
Put "waiting for permission" somewhere you'll see it. It doesn't need a Stream Deck — a colored corner of the screen or a sound works. For me, the time a session spent waiting on me was the least visible and the most wasted.
I haven't measured how much time this saves, so I won't claim a number. What I can say is that I stopped hunting through windows.
Japanese version (note): https://note.com/modern_elk8749/n/n516084403556
A personal project by a Claude Code fan. Not affiliated with Anthropic or Elgato.
Top comments (1)
There is a second way to get the stuck blue key, separate from the EPERM rename. The Stop hook does not fire when you interrupt a turn yourself: the hooks reference says Stop does not run if the stoppage came from a user interrupt. Press Esc on a blue session and the key stays blue until the next Stop, or until an idle_prompt arrives.
Your idle_prompt mapping may catch it, but idle_prompt only fires after about 60 seconds and only when you appear to be away from the terminal. A faster check on the plugin side: treat "running" as stale when the session's transcript JSONL has not been written for some number of seconds. Every hook payload carries
transcript_path, so the hook can write that path into its JSON and the plugin can check the file's mtime without reading any prompt text, which keeps your "no prompt text, no tool output" rule intact.One caveat: skip that check while a PreToolUse has no matching PostToolUse yet. A long build writes nothing to the transcript and would otherwise turn the key orange mid-run.