agent-status 2.0.0

Tmux-integrated indicator showing which AI coding agent sessions are waiting on user input.
Documentation

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, omp, 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

cargo install agent-status

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:

cargo install agent-switcher

Configure

Claude Code

Drop this alias into your shell rc (.zshrc, .bashrc, etc.):

alias claude='claude --settings "$(agent-status agent-extension --agent claude-code)"'

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:

{
  "hooks": {
    "Notification": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "agent-status set --agent claude-code notify"
          }
        ]
      }
    ],
    "PermissionRequest": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "agent-status set --agent claude-code notify"
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "agent-status set --agent claude-code done"
          }
        ]
      }
    ],
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "agent-status set --agent claude-code working"
          }
        ]
      }
    ],
    "PreToolUse": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "agent-status set --agent claude-code working"
          }
        ]
      }
    ],
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "agent-status set --agent claude-code idle"
          }
        ]
      }
    ],
    "SessionEnd": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "agent-status clear --agent claude-code"
          }
        ]
      }
    ]
  }
}

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.):

alias pi='pi -e "$(agent-status agent-extension --agent pi)"'

Each pi invocation regenerates ${XDG_RUNTIME_DIR:-/tmp}/agent-status/extensions/pi.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
session_start set idle (session visible from launch)
before_agent_start set working (activity = first line of the prompt)
tool_execution_start set working, or set notify for the ask tool and for tools pi will show an approval dialog for
tool_call set working (fires at approval-granted; skipped for ask)
tool_execution_end set working (returns from notify once answered/denied)
agent_end set done (activity = assistant's last message)
session_switch / session_branch clear the previous session's row, set idle for the new one
session_shutdown clear (removes the row)

Notify semantics: pi has no shell hook for "agent paused for permission", but the bridge reconstructs it from two sources: the built-in ask tool (always notify, with the question as the activity message) and a prediction of pi's tool-approval dialog read from ~/.pi/agent/settings.json (tools.approvalMode: always-ask / write / yolo, plus per-tool tools.approval.<tool> overrides). In the default yolo mode only ask produces notify. The write-mode prediction uses a built-in read-tier list (read, search, find) and errs toward over-reporting for unknown tools.

omp (oh-my-pi)

omp is a coding-first fork of pi and keeps pi's per-launch -e <file> extension flag, so the same alias pattern applies. Drop this into your shell rc:

alias omp='omp -e "$(agent-status agent-extension --agent oh-my-pi)"'

Each omp invocation regenerates ${XDG_RUNTIME_DIR:-/tmp}/agent-status/extensions/oh-my-pi.ts with the absolute path to the current agent-status binary baked into the bridge's BIN constant, alongside whatever else you have under ~/.omp/agent/extensions/.

The extension fires on these omp lifecycle events:

omp event agent-status call
session_start set --agent oh-my-pi idle
session_switch, session_branch clear for the previous session id, then set idle for the new one
before_agent_start set --agent oh-my-pi working (activity = first line of prompt)
tool_execution_start set --agent oh-my-pi working (activity string); ask tool → notify
tool_execution_end set --agent oh-my-pi workingask tool only (closes the gap)
agent_end set --agent oh-my-pi done (activity = assistant's last text)
session_shutdown clear --agent oh-my-pi

omp's built-in ask tool — the agent explicitly asking you a question — is its canonical "waiting for your input" signal and maps to notify, so omp sessions get the full Claude Code state vocabulary (idle / working / notify / done). tool_execution_end → working fires for the ask tool only: omp emits no event when you answer an ask prompt, so the end of the ask tool's execution is what flips the row out of notify. Other tools deliberately skip it — every set overwrites the activity message, and omp spends most wall-clock thinking between fast tool calls, so wiping on every tool end (Claude Code's PostToolUse behavior) would leave the Activity column blank almost always. Skipping it keeps the last tool's activity visible through the thinking gap, like the pi bridge.

Known limitation: omp's tool-approval gate (tools.approvalMode: always-ask or write) runs before any extension event fires, so approval prompts can't be surfaced as notify. omp's default mode is yolo (no approval prompts), which makes this gap mostly theoretical; if you run a prompting approval mode, the row stays at working while an approval dialog is open.

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:

mkdir -p ~/.config/opencode/plugins
cp "$(agent-status agent-extension --agent opencode)" ~/.config/opencode/plugins/agent-status.ts

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

agent-status --help                       # top-level help
agent-status set [EVENT] --agent NAME     # mark this session as waiting (reads JSON on stdin)
agent-status clear --agent NAME           # clear this session's state (reads JSON on stdin)
agent-status status                       # print the status-right line, empty if nothing waiting
agent-status list                         # print TSV (session_id, pane, display) per waiting session

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:

{
  "agent": "claude-code",
  "project": "agent-status",
  "cwd": "/path/to/project",
  "event": "notify",
  "tmux_pane": "%17",
  "ts": 1778163565,
  "message": "Permission required",
  "pid": 12345
}

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", "oh-my-pi", "opencode", or "pi"; 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 Stop hook 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 the Stop line from settings.json if it proves too eager.
  • opencode has no "user submitted a prompt" event, so once session.idle marks an opencode session done, the indicator stays on done while you type the next prompt and only refreshes its timestamp on the next idle. Same intent as the Stop caveat 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).
  • opencode does not record a working state today. opencode sessions appear in the switcher when they're waiting (done / notify) but not while they're mid-turn. Claude Code, pi, and omp all record working and idle.

License

MIT. See LICENSE.