agent-status
A small Rust CLI + library that shows in tmux's status-right which AI coding
agent sessions are waiting on user input. Supports Claude Code, pi, and
opencode; the architecture is set up to plug in additional agents (Codex CLI,
Cursor CLI) without restructuring.
$ agent-status status # one session waiting
[!] agent-status
$ agent-status status # multiple sessions waiting
[!] 3 projects waiting
$ agent-status status # nothing waiting
# (no output, exit 0)
How it works
Claude Code hooks fire agent-status set notify on Notification and
PermissionRequest, set done on Stop, set working on UserPromptSubmit
and PreToolUse, set idle on SessionStart, and clear on SessionEnd.
Each set writes one JSON file per session under
${XDG_RUNTIME_DIR:-/tmp}/agent-status/, keyed by session_id. tmux's
status-right invokes agent-status status on its refresh interval; the
command lists the state directory and renders the indicator (project name for
one waiting session, count for many, nothing for none).
No daemon. The filesystem is the state store; each session writes only its own keyed file, so concurrent writers never contend.
Install
The binary lands in ~/.cargo/bin. As long as that's on your $PATH (Cargo's
installer adds it by default), the alias-based wiring below works without any
further configuration. The binary is around 550 KB stripped and has no runtime
dependencies (tmux is invoked best-effort to refresh the status bar; if it isn't
running, the failure is silenced).
For the popup picker, also install the companion crate:
Configure
Claude Code
Drop this alias into your shell rc (.zshrc, .bashrc, etc.):
That's it. Each time you run claude, the alias expands so claude launches with
--settings <generated.json> — a seven-hook file that agent-status
regenerates on every invocation from its own absolute path. Claude Code merges
--settings on top of your user/project settings, so nothing you've already
configured gets overwritten.
The generated file lives at
${XDG_RUNTIME_DIR:-/tmp}/agent-status/extensions/claude-code.json and is
rewritten every run — no cleanup, no env vars, no PATH manipulation.
Wiring the hooks manually (fallback)
If you'd rather see the hooks in your settings file, skip the alias and merge
this into the top-level hooks block of ~/.claude/settings.json instead:
Pick one route, not both. If you have manual hooks AND the alias, each event
fires twice — idempotent for repeats of the same set but wasteful.
SessionStart writes a placeholder idle row so every Claude session appears
in the agent-switcher popup from the moment it starts — even
before you type the first prompt. UserPromptSubmit and PreToolUse then flip
the row to working while Claude is mid-turn. The tmux status indicator filters
both idle and working out, so the bar still only shows "needs you now"
sessions (notify from Notification/PermissionRequest, done from Stop).
The switcher shows every row, rendering idle as a dim dot and working as a
spinner.
pi
Drop this alias into your shell rc (.zshrc, .bashrc, etc.):
Each pi invocation regenerates
${XDG_RUNTIME_DIR:-/tmp}/agent-status/extensions/pi-coding-agent.ts with the
absolute path to the current agent-status binary baked into the bridge's BIN
constant. pi's -e <path> flag loads the file as a one-shot extension,
alongside whatever else you have under ~/.pi/agent/extensions/.
The extension fires on these pi lifecycle events:
| pi event | agent-status call |
|---|---|
before_agent_start |
clear --agent pi-coding-agent (user submitted a prompt) |
agent_end |
set --agent pi-coding-agent done (agent finished a turn) |
session_start |
clear --agent pi-coding-agent |
session_shutdown |
clear --agent pi-coding-agent |
Known limitation: pi has no built-in "agent paused waiting for permission"
event analogous to Claude Code's PermissionRequest hook — pi extensions handle
confirmations in-process via ctx.ui.confirm(). So pi-coding-agent surfaces the
"done" state but not a separate "needs attention" state. In practice the
dominant signal is "agent finished a turn, waiting on next prompt" anyway.
opencode
opencode discovers plugins from ~/.config/opencode/plugins/ (global) or
.opencode/plugins/ (per-project) at startup — there's no per-launch extension
flag, so the alias pattern can't apply. Generate the plugin and copy it once:
agent-status agent-extension --agent opencode writes a regenerated
${XDG_RUNTIME_DIR:-/tmp}/agent-status/extensions/opencode.ts with the absolute
path to the current agent-status binary baked in, and prints that path. The
cp lands a fresh copy in opencode's discovery directory. Re-run this whenever
you move or rebuild the agent-status binary so the baked-in path stays
correct.
The plugin fires on these opencode events:
| opencode event | agent-status call |
|---|---|
session.idle |
set --agent opencode done (agent finished a turn) |
permission.updated |
set --agent opencode notify (agent paused for permission) |
session.created |
clear --agent opencode |
session.deleted |
clear --agent opencode |
In practice opencode persists sessions for resume, so session.deleted rarely
fires on graceful exit — the clear arm is defensive. session.created
likewise fires once at the start of each new session and resolves to a no-op
clear (no state file to remove yet); it exists so a stale state file from a
previous crash gets dropped at session start.
Unlike pi, opencode emits a permission.updated event when an agent pauses for
a permission prompt, so opencode supports both notify and done indicator
states (full feature parity with Claude Code). The one wart: opencode has no
event for "user submitted a prompt", so after a turn ends the indicator stays on
done while the user types the next prompt — by design, since the session is
the one that needs your attention.
tmux (~/.tmux.conf)
Drop #(agent-status status) into your existing status-right wherever you
want the indicator to appear, and lower the refresh interval so updates feel
snappy:
set -g status-interval 5
set -g status-right "#(agent-status status) <your existing status-right here>"
Reload with tmux source-file ~/.tmux.conf.
For the popup picker (prefix + C-a), see the agent-switcher
crate.
Usage
set and clear expect a JSON object on stdin with at least
{"session_id": "..."}. Empty or missing session_id is a silent no-op.
State location
${XDG_RUNTIME_DIR:-/tmp}/agent-status/<session_id> — one file per active
session. Inspectable with ls/cat. Format:
The message field is optional and only present when the agent's hook payload
supplies one (e.g. Claude Code's Notification event). Older state files
written before this field existed still load — message defaults to absent.
The agent field is "claude-code", "opencode", or "pi-coding-agent"; new
agents use their own lowercase-hyphenated name.
The pid field records the agent process's PID so agent-status status and
list can detect and remove entries whose owning process has died without
firing its session-end hook. Files written by older binaries — which lack pid
— are never auto-pruned; they age out only on tmpfs cleanup. Such entries should
disappear naturally after one set/clear cycle on the affected session.
Caveats
- The
Stophook fires on every turn end, so any session that just finished a response shows up as "waiting" until you send the next prompt. Intentional — the whole point is to know which session needs you while you're heads-down elsewhere. Drop theStopline fromsettings.jsonif it proves too eager. - opencode has no "user submitted a prompt" event, so once
session.idlemarks an opencode sessiondone, the indicator stays ondonewhile you type the next prompt and only refreshes its timestamp on the next idle. Same intent as theStopcaveat above — the session is the one waiting on you. - Architecture-specific binary. The compiled binary is platform-locked. On a
new machine, rebuild from source (
cargo install agent-status). - Only Claude Code records a
workingstate today. pi and opencode sessions appear in the switcher when they're waiting (done/notify) but not while they're mid-turn. The hook semantics for those agents can be extended in a follow-up.
License
MIT. See LICENSE.