agent-status 2.1.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.

```text
$ 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][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

```sh
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:

```sh
cargo install agent-switcher
```

## Configure

### Claude Code

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

```sh
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:

```json
{
  "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`][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.):

```sh
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:

```sh
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 working``ask` 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:

```sh
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:

```tmux
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`][switcher]
crate.

## Usage

```sh
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) for every session; marker reflects state
```

`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:

```json
{
  "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](LICENSE).

[hooks]: https://docs.claude.com/en/docs/claude-code/hooks
[Claude Code]: https://claude.com/claude-code
[pi]: https://pi.dev
[omp]: https://omp.sh
[opencode]: https://opencode.ai
[switcher]: https://crates.io/crates/agent-switcher