Teaching tmux to babysit my Claude Code agents A developer built a tmux-based notification system that displays colored dots in window tabs to indicate when Claude Code agents need attention. The system uses Claude Code hooks to write status flags to tmux window variables, showing amber for blocked agents needing permission or input, green for completed tasks, and no dot for agents still working. The solution allows developers running multiple Claude Code sessions across tmux windows to glance at their terminal and immediately identify which agents require human intervention. Teaching tmux to babysit my Claude Code agents If you are like me, you no longer run one Claude Code session — you run a small fleet. One window is building a feature in a TypeScript monorepo, another is reviewing a colleague’s pull request, a third is chasing a redraw bug in a Neovim plugin. tmux makes this easy: a window per agent, Alt + 1 – 9 to jump between them. Often they are not even different projects — just git worktrees of the same repo, one agent per branch. The trouble starts at three or four agents. You burn time cycling through windows playing twenty questions with yourself: is this one still working? Has that one stopped to ask me something? Did the one from ten minutes ago finish, or is it waiting on me to approve a git push ? An agent is autonomous right up until the moment it needs you — and out of the box it has no way to tap you on the shoulder. Hacker News is full of the fix: a new wave of agent-runner tools, many with a column of vertical tabs, one per agent, each lighting up when it wants attention — Cursor’s v3 rewrite https://news.ycombinator.com/item?id=47618084 , an “IDE for the agents era” https://news.ycombinator.com/item?id=48236770 , “sandboxed coding agents for a team” https://news.ycombinator.com/item?id=48225040 and Google’s Antigravity https://news.ycombinator.com/item?id=48234090 , now on its second version. These do far more than light up a tab — PR workflows, GitHub integration, sandboxing, team orchestration — and I will probably borrow ideas from them. But I live in tmux all day, and all I wanted was the small part: a glanceable sense of which agent needs me. That does not need a new IDE, just the windows I already have open. So I taught tmux to show it. Every window now carries a coloured dot for the Claude Code session inside it: amber — blocked, needs me a permission prompt or a question ; green — finished, with a response waiting to be read; nothing — working away, or nothing to report. The green dot clears itself the instant I switch to that window — by the time I am reading, it is gone. The amber dot is stubborner: it stays until I have actually dealt with the request, because glancing at a window is not the same as answering it. It is two halves that never touch directly — they meet on a single tmux variable. Claude Code hooks write it; a tmux format string reads it back and paints the dot. I drive it through home-manager in my nix-meridian https://github.com/StanAngeloff/nix-meridian config, so the snippets are Nix, but the substance is the shell and tmux config — lift those straight out. Half one: Claude Code flags the window Claude Code hooks https://code.claude.com/docs/en/hooks run a shell command at set points in a session. The ones I use: PermissionRequest — wants to do something unauthorised, waiting on a yes/no. Elicitation — asking you a structured question. Stop / StopFailure — the turn ended. PostToolUse — a tool just finished. UserPromptSubmit — you sent a new prompt. SessionEnd — Claude exited. The glue is one variable: $TMUX PANE . tmux sets it in every pane and Claude Code inherits it, so a hook always knows which window it is running in. Flagging that window is one command: bash Flag this pane's window as needing attention: $ tmux set -w -t "$TMUX PANE" @claude-state permission ...and clear it: $ tmux set -wu -t "$TMUX PANE" @claude-state @claude-state is a custom option — tmux lets you invent any name starting with @ . -w scopes it to the window and -u unsets it. One string per window, holding permission , elicitation , idle or nothing. I wrap set, clear and a conditional clear into a small Nix helper: js tmux-claude-state = let tmux = "${lib.getBin pkgs.tmux}/bin/tmux"; in { Set @claude-state to a given value on this pane's window. set = state: '' -n "$TMUX PANE" && ${tmux} set -w -t "$TMUX PANE" @claude-state ${state} || true''; Clear it, whatever it was. reset = '' -n "$TMUX PANE" && ${tmux} set -wu -t "$TMUX PANE" @claude-state || true''; Clear it only if we are currently blocked — don't clobber anything else. reset-blocked = '' -n "$TMUX PANE" && case "$ ${tmux} show -wv -t "$TMUX PANE" @claude-state 2 /dev/null " in permission|elicitation ${tmux} set -wu -t "$TMUX PANE" @claude-state ;; esac; true''; }; The -n "$TMUX PANE" guard makes it a no-op outside tmux; the trailing || true stops a failed tmux call surfacing as a hook error. Then wire them up trimmed — the full set is in the repo : hooks = { PermissionRequest = { hooks = { type = "command"; command = "...play a chime..."; } more on this later { type = "command"; command = tmux-claude-state.set "permission"; } ; } ; Stop = { hooks = { type = "command"; command = tmux-claude-state.set "idle"; } ; } ; PostToolUse = { hooks = { type = "command"; command = tmux-claude-state.reset-blocked; } ; } ; UserPromptSubmit = { hooks = { type = "command"; command = tmux-claude-state.reset; } ; } ; }; Elicitation mirrors PermissionRequest without the chime 1 , StopFailure mirrors Stop , SessionEnd mirrors UserPromptSubmit . The logic: blocked PermissionRequest / Elicitation → permission / elicitation — amber. finished Stop / StopFailure → idle — green. tool ran PostToolUse → clear it if we were amber; you have just approved it. new prompt / session end → clear everything. Nothing flags “working” — that is the default, and a row of nine busy dots would just be noise. Half two: tmux paints the dot tmux renders the variable in window-status-format , the template for every inactive window: setw -g window-status-format \ " I│ {? {@claude-state}, {? {==: {@claude-state},idle}, fg=colour34 ● fg=colour247 , fg=colour214 ● fg=colour247 },} W " tmux’s format syntax https://man.openbsd.org/tmux.1 FORMATS is dense: {?cond,then,else} is a ternary, and they nest. Unfolded: {? {@claude-state}, is @claude-state set at all? {? {==: {@claude-state},idle}, yes — is it exactly "idle"? fg=colour34 ● fg=colour247 , yes → green dot, then back to grey fg=colour214 ● fg=colour247 }, no → amber dot, then back to grey } not set → render nothing No @claude-state and you get an empty string, so the name renders as normal. Otherwise an exact match on idle picks the colour. ● is a Unicode circle; fg=... sets the colour on either side of it. The indices are the plain 256-colour palette: colour34 green “done, come and look” , colour214 amber “I need you” and colour247 the grey for inactive text, restored after the dot. A traffic light, hiding in a status bar. Clearing the green dot on focus A green dot is only useful until you look, so I clear it when you select the window: When you switch to a window, if its Claude state is the green "idle" flag, clear it — you are about to read the response anyway. set-hook -g after-select-window \ 'if -F " {==: {@claude-state},idle}" "set -wu @claude-state"' It only clears idle , never permission / elicitation . Green means “something to read”, and switching there reads it; amber means “something to do ”, which a glance does not. The amber dot survives until the work happens PostToolUse → reset-blocked or you send a new prompt. One last nicety: the focused tab uses a separate window-status-current-format with no dot logic at all — if you are looking at a window, you do not need telling what is in it. So dots only ever appear on the windows you are not in. Start to finish - I send a prompt. UserPromptSubmit clears the flag — no dot. - Claude works. Nothing touches the state — still no dot. - It hits a git push it cannot run unattended. PermissionRequest fires: chime, amber tab. - I approve it — the amber dot stayed, since glancing didn’t clear it. The command runs and PostToolUse clears the flag. - The turn ends. Stop turns the tab green. - I switch over to read it. after-select-window clears the green dot before I finish the first line. Ring a bell Dots work when your eyes are on the terminal. They do not when you have wandered off. So the blocking case — and only that — also makes a noise: PermissionRequest = { hooks = { type = "command"; command = "${lib.getBin pkgs.pipewire}/bin/pw-play ${./audio/notifications/mixkit-clear-announce-tones-2861.mp3}"; timeout = 5; } { type = "command"; command = tmux-claude-state.set "permission"; } ; } ; pw-play is PipeWire’s player; the clip is a short Mixkit https://mixkit.co/free-sound-effects/ tone vendored into the repo, so nothing reaches the network just to make a sound. Use whatever you like. It rings on PermissionRequest only, never on Stop . A chime on every finished turn would be nine an hour and muted by lunch; a chime only when an agent is genuinely stuck is worth keeping on. This is what lets me walk away — start three agents, go and make a coffee, come back when one of them calls. The rest carry on. Two rough edges It is not perfect: Forked subagents sometimes flip a window amber for nothing. With subagent forking on, spawning one occasionally turns the window amber when nothing is waiting. Something inside the fork trips a blocking hook; I have not pinned down what yet. The amber dot clears a beat late. I clear it on PostToolUse — when the tool finishes , not when you approve it. Green-light a long build or a sleep 30 and the window stays amber for the whole run, though it stopped needing you the second you said yes. A PreToolUse hook would clear it sooner; that one is on the list. Wrapping up That is the whole thing: the see-every-agent, ping-me-when-one-needs-you experience the big tools are built around, except this slice of it lives in the terminal I already work in. A dozen lines of hooks, one format string and a sound file. I am not the only one who wants this — Solo https://soloterm.com/ is a lovely native workspace Tauri, not Electron that runs all your agents with status indicators built in, and it is well worth a look. It does far more than my handful of dots; it is just more than I need. If you would rather have the whole workspace handed to you, that is what it is for. None of the clever part is Claude-specific — it is hooks writing a variable and a status bar reading it. Point tmux set -w @claude-state at a long make or a deploy and you get the same dots for free. The full version lives in my nix-meridian https://github.com/StanAngeloff/nix-meridian config under home/apps/tmux and home/apps/claude-code . Steal it, and never go window-hunting again. Happy hacking Footnotes - Both blocking states — a permission request and an elicitation — show the same amber dot, but only PermissionRequest rings the bell; that is the one that tends to catch me mid-coffee. ↩ user-content-fnref-1