Shows Claude Code's state (working, waiting for you, done) as coloured glyphs on your tmux window tabs.

A Claude Code mod that shows what each Claude session is doing in your tmux status bar, so you can see at a glance which windows are busy, which need you and which have finished.

| Glyph | State | Shown |
|---|---|---|
| <img src="docs/working.svg" width="16" height="16" alt="yellow dot"> yellow | working on a turn | always |
| <img src="docs/waiting.svg" width="16" height="16" alt="red dot"> red | waiting for you: a permission prompt, a question, an MCP elicitation | always |
| <img src="docs/done.svg" width="16" height="16" alt="green tick"> green | finished its turn | only on windows you are not looking at |
Outside tmux the mod does nothing.
1. The mod. From GitHub:
claude plugin marketplace add XavierYounan/claude-code-tmux-status
claude plugin install tmux-status@tmux-status
Or from a local clone, for one session: claude --plugin-dir /path/to/claude-code-tmux-status.
2. The tmux side. With tpm, add this after your theme's @plugin line (themes usually overwrite the window formats, so this needs to load after them), then press prefix + I:
set -g @plugin 'XavierYounan/claude-code-tmux-status'
Without tpm, clone the repo and source the snippet after your theme loads:
source-file /path/to/claude-code-tmux-status/tmux/claude-status.tmux
That prefixes the glyph to every window tab. To put it somewhere else, comment out the two if -F lines at the bottom of that file and drop #{E:@claude_status} into your own format. Colours are the @claude_icon_* options, so you can override them after sourcing.
The mod hooks Claude Code's turn, permission and tool events and runs tmux set-option -w -t $TMUX_PANE @claude_state <state>. Everything visual lives in tmux formats, so it works with any theme. The done state stays until your next prompt, but the format only shows it on inactive windows.
One state per window: if you run two Claude sessions in split panes of the same window, the last one to change wins.
Mods (Claude Code's function-hooks plugins) are early access, and their API can change between releases. Built and tested against Claude Code 2.1.289; if it stops working after an update, please open an issue.
claude plugin validate .
claude plugin test .hooks/register.ts 80 lines1import type { EngineInterface, Register } from 'claude-code'
2
3// What the window option @claude_state holds. Unset means no Claude session
4// in that window (or one that has not started a turn yet).
5export type State = 'working' | 'waiting' | 'done'
6
7const OPTION = '@claude_state'
8
9type $ = EngineInterface
10
11// The pane this Claude Code runs in; undefined outside tmux, where every hook
12// below does nothing.
13const paneOf = ($: $) => $.env.get('TMUX_PANE')
14
15const tmux = ($: $, ...argv: string[]) => $.process.run(['tmux', ...argv], { timeoutMs: 2000 })
16
17async function setState($: $, state: State | undefined) {
18 const pane = await paneOf($)
19 if (pane === undefined) return
20 await (state === undefined
21 ? tmux($, 'set-option', '-wqu', '-t', pane, OPTION)
22 : tmux($, 'set-option', '-wq', '-t', pane, OPTION, state))
23}
24
25export const register: Register = on => {
26 on('session.start', async ($, e, next) => {
27 const started = await next(e)
28 await setState($, undefined)
29 return started
30 })
31
32 on('session.end', async ($, e, next) => {
33 await setState($, undefined)
34 return next(e)
35 })
36
37 // Main-loop turns only: a subagent's run raises no turn.start, and its
38 // turn.complete carries an agentId.
39 on('turn.start', async ($, e, next) => {
40 await setState($, 'working')
41 return next(e)
42 })
43
44 on('turn.complete', async ($, e, next) => {
45 const done = await next(e)
46 if (e.agentId === undefined) await setState($, 'done')
47 return done
48 })
49
50 // Blocked on the person: a permission dialog, an MCP elicitation, or a
51 // question the model asked. Answering it resumes the turn.
52 on('classic.PermissionRequest', async ($, e, next) => {
53 await setState($, 'waiting')
54 return next(e)
55 })
56 on('classic.Elicitation', async ($, e, next) => {
57 await setState($, 'waiting')
58 return next(e)
59 })
60 on('classic.PermissionDenied', async ($, e, next) => {
61 await setState($, 'working')
62 return next(e)
63 })
64 on('classic.PostToolUse', async ($, e, next) => {
65 await setState($, 'working')
66 return next(e)
67 })
68 on('classic.ElicitationResult', async ($, e, next) => {
69 await setState($, 'working')
70 return next(e)
71 })
72
73 on('tool.call', { tool: 'AskUserQuestion' }, async ($, e, next) => {
74 await setState($, 'waiting')
75 const answered = await next(e)
76 await setState($, 'working')
77 return answered
78 })
79}
80