See when an AI agent needs you without hunting through panes.
gentle-agent-state connects AI coding agents to your terminal session. Agents
emit lifecycle events, this project normalizes them into working, blocked, and
idle, then shows the state in tmux, Zellij, or native Ghostty.
- tmux: colored state markers in the window/tab bar, rolled up from all panes.
- Zellij: the tab title can roll up agent state; the exact agent pane title changes too.
- Ghostty: the native window/tab title shows the latest agent state and plays transition sounds.
- Agents: opencode, pi, Claude Code, and Codex.
1 api o 2 claude x 3 notes
(orange) (red, beeps) (idle)
git clone https://github.com/Gentleman-Programming/gentle-agent-state.git
cd gentle-agent-state
./install.sh --allThen restart your agents inside tmux, Zellij, or Ghostty.
For tmux, either open a fresh tmux session or reload your config:
tmux source-file ~/.config/tmux/tmux.confWant only specific agents?
./install.sh --with-opencode --with-pi
./install.sh --with-claude --with-codex| State | Meaning | tmux display | Zellij display | Ghostty display | Alert |
|---|---|---|---|---|---|
working |
Agent is running | o in orange |
pane title: o |
title marker: o |
none |
blocked |
Agent is waiting for you | x in red |
pane title: x |
title marker: x |
sound + flash/message in tmux/Zellij, sound in Ghostty |
idle |
Agent finished or is not running | no marker | pane title restored | marker removed | sound after busy state |
The window state marker shows the worst state across panes:
blocked > working > idle
So if any pane in a tmux window is blocked, that window shows x in red.
Zellij does not expose tmux-style window user options, so the backend uses native pane renaming actions. By default it does not rename tabs, preserving user-managed tab names exactly; the exact agent pane title shows its own state.
If you prefer tab-title rollup, opt in with AGENT_ZELLIJ_RENAME_TAB=1. In that
mode the tab title appends the worst state across panes in that tab, and the
backend best-effort restores the original tab name when the tab returns to idle.
Native Ghostty support uses TERM_PROGRAM=ghostty, updates the terminal title
with OSC escape sequences, and plays the same best-effort transition sounds as
other backends: blocked sound when the state becomes blocked, and idle sound
when working returns to idle. A direct blocked → idle transition stays
quiet to avoid double-playing blocked and success sounds for prompts.
Ghostty's OSC title sequence sets the whole title; it does not expose the same
tab metadata API that Zellij does. The backend therefore tries to query and cache
the current terminal title, then appends only the state marker: o for working,
x for blocked, and no marker for idle. If title querying is unavailable in your
environment, set a stable base title with AGENT_GHOSTTY_TITLE_BASE, for example:
export AGENT_GHOSTTY_TITLE_BASE="my-project"That produces my-project o, my-project x, and restores my-project on idle.
If Ghostty can report the current title, no base variable is needed: an existing
my-project title becomes my-project o or my-project x and is restored on
idle. If you run tmux or Zellij inside Ghostty, the tmux/Zellij backend still
takes precedence.
| Agent | Install flag | Install target |
|---|---|---|
| opencode | --with-opencode |
~/.config/opencode/plugins/gentle-agent-state.js |
| pi | --with-pi |
~/.pi/agent/extensions/gentle-agent-state.ts |
| Claude Code | --with-claude |
merges hooks into ~/.claude/settings.json |
| Codex | --with-codex |
merges hooks into ~/.codex/hooks.json |
| all detected | --all |
every detected agent config directory |
Adapters are opt-in. The installer only touches agents you request, except
--all, which selects agents whose config directories already exist.
Claude/Codex hook merging is append-only and idempotent. Existing hooks are kept.
Legacy tmux-agent-state hook commands are migrated to the neutral
gentle-agent-state core path.
Required:
bashjqpython3- at least one display backend:
tmux,zellij, or Ghostty
Optional sound players:
- macOS:
afplay - Linux/BSD:
paplay,canberra-gtk-play, oraplay
No sound player? Nothing breaks; alerts just become visual/state-only.
| Env var | Default | Effect |
|---|---|---|
AGENT_SOUND_BLOCKED |
macOS Funk.aiff, Linux dialog-warning.oga |
sound when an agent becomes blocked |
AGENT_SOUND_IDLE |
macOS Glass.aiff, Linux complete.oga |
sound when a busy agent finishes |
HERDR_ENV=1 |
unset | disables every adapter so Herdr can own integration |
tmux state marker colors live in tmux/agents.conf:
| State | Color |
|---|---|
| blocked | #e82424 |
| working | #dca561 |
Edit those values if your theme needs different colors.
Every agent has its own event dialect. gentle-agent-state keeps that complexity at the edge with thin adapters:
opencode ┐
pi ├──▶ agent-report.sh ──▶ tmux backend
Claude │ ├─▶ Zellij backend
Codex ┘ └─▶ Ghostty title + sound backend
The core vocabulary is deliberately small:
| Canonical state | Example source event |
|---|---|
working |
prompt submitted, tool started, session active |
blocked |
permission request, user question, approval needed |
idle |
stop, turn complete, session idle |
This is an anti-corruption layer: adding a new agent should require one adapter, not changes to every multiplexer backend.
| Path | Purpose |
|---|---|
~/.config/agent-state/scripts/agent-report.sh |
neutral dispatcher |
~/.config/agent-state/scripts/tmux-agent-report.sh |
tmux backend |
~/.config/agent-state/scripts/zellij-agent-report.sh |
Zellij backend |
~/.config/agent-state/scripts/ghostty-agent-report.sh |
Ghostty title + sound backend |
~/.config/agent-state/scripts/hook-adapter.sh |
Claude/Codex hook adapter |
| Path | Purpose |
|---|---|
~/.config/tmux/agents.conf |
tab state markers, hooks, visual bell |
~/.config/tmux/scripts/agent-status.sh |
clears stale blocked states when panes become visible |
~/.config/tmux/scripts/agent-statusline.sh |
keeps the self-heal heartbeat in status-right |
Check that you restarted the agent process after installing the adapter. Most agents load plugins/extensions only on startup.
Reload tmux config:
tmux source-file ~/.config/tmux/tmux.confThen confirm your config sources the generated file:
grep agents.conf ~/.config/tmux/tmux.conf ~/.tmux.conf 2>/dev/nullConfirm the agent is running inside Zellij and has a pane id:
echo "$ZELLIJ_PANE_ID"
zellij action rename-pane --pane-id "$ZELLIJ_PANE_ID" "test-pane"
zellij action undo-rename-pane --pane-id "$ZELLIJ_PANE_ID"
zellij action current-tab-info
zellij action rename-tab "test-tab"
zellij action undo-rename-tabConfirm the agent is running directly inside Ghostty:
echo "$TERM_PROGRAM"
printf '\033]2;ghostty-title-test\007'If you are running tmux or Zellij inside Ghostty, troubleshoot that backend instead; tmux/Zellij take precedence over the native Ghostty title backend.
Re-run the installer for that agent and inspect the hook file:
./install.sh --with-claude
jq '.hooks' ~/.claude/settings.json
./install.sh --with-codex
jq '.hooks' ~/.codex/hooks.jsonSet HERDR_ENV=1. All adapters exit early and let Herdr own the integration.
./uninstall.shThis removes:
- the neutral core scripts, including tmux/Zellij/Ghostty backends;
- tmux
agents.confand generated tmux scripts; - the tmux source line from
~/.config/tmux/tmux.confor~/.tmux.conf; - opencode/pi adapter files;
- Claude/Codex hooks added by this project.
Other hooks and user configuration are preserved.
Run quick checks before opening a PR:
bash -n install.sh uninstall.sh scripts/*.sh tmux/scripts/*.sh
node --check adapters/opencode/gentle-agent-state.js
bash tests/ghostty-contract.shA useful smoke test is installing into a temporary home:
tmp="$(mktemp -d)"
HOME="$tmp" ./install.sh
HOME="$tmp" ./uninstall.shMIT — see LICENSE.