Agent hooks
The built-in
hooks
extension wires each coding agent's lifecycle hooks to
thurbox-cli session signal
so every session reports its state back to Thurbox. The session list then shows, at a
glance, which agents are
working
,
blocked
,
done
, or
idle
— a colored dot per row, rolled up per repo group.
On by default. Unlike other extensions , hooks ships embedded in the binary and is auto-activated on first run — the default agent's hook is pre-configured with zero setup. It installs through the ordinary extension machinery (it just lives inside Thurbox instead of being fetched).
Overview #
The hook command is always
thurbox-cli session signal --state <working|blocked|done|idle>
, suffixed
|| true
so it can never break the agent. Identity comes from the injected
$THURBOX_SESSION
environment variable, so a hook passes
no ids
. The reported state is persisted on the session row and picked up by the TUI within a
tick (via
PRAGMA data_version
), so a hook firing headlessly is honored too.
These are the hooks thurbox installs
into
the agents. The other direction — your own commands that thurbox runs when it
creates, deletes, restarts or restores a session — is
hooks.toml
(session lifecycle hooks), a file that
sits beside this extension's
hooks/
directory and shares nothing with it but the word.
The five states #
| State | Colour | Glyph | Meaning |
|---|---|---|---|
Working |
yellow | animated braille spinner (⠋⠙⠹…) |
the agent is actively running |
Blocked |
red | ◆ |
the agent needs input or approval |
Done |
blue | ● |
a turn just finished; shown until you switch away |
Idle |
green | ○ |
acknowledged, never active, or at rest |
Error |
red | ✗ |
reserved for a crashed agent (not derived yet) |
A
Done
session becomes
Idle
once you move focus off it (you've acknowledged it); a
Working
session that goes quiet for 10 s is treated as
Idle
so an interrupted turn never spins forever. Repo groups roll up to their most-urgent
member (
Blocked > Error > Working > Done > Idle
), and the status colours are tunable theme keys.
Per-agent coverage #
How much each agent can report depends on the lifecycle surface its CLI exposes — Thurbox maps whatever hooks an agent offers, and the dot is accurate for the edges that agent can express:
| Agent | idle | working | blocked | done |
|---|---|---|---|---|
| claude | ✓ | ✓ | ✓ | ✓ |
| opencode | ✓ | ✓ | ✓ | ✓ |
| codex * | ✓ | ✓ | — | ✓ |
| antigravity | ✓ | ✓ | ✓ | ✓ |
| aider | — | — | ✓ | — |
| copilot * | ✓ | ✓ | ✓ | ✓ |
| vibe * | — | ✓ | — | ✓ |
| pi * | ✓ | ✓ | ✓ | ✓ |
| omp * | ✓ | ✓ | ✓ | ✓ |
*
Experimental
— the hook schema for codex, copilot, vibe, pi, and omp is newer or not
exhaustively verified against the live CLI. If event names differ, the fix is a one-file edit to the
extension payload (no code change), and an unrecognized hook is a fail-open no-op
(
|| true
) so the agent is never broken.
How each agent is wired #
-
claude
— a managed settings file passed via
--settings(claude merges it with your own settings, never clobbering them).SessionStart→ idle,UserPromptSubmit/PreToolUse→ working,Stop→ done, andNotification→ blocked only for permission/approval prompts (an idle nudge is ignored so a session never flips to a false red). -
opencode
— a plugin dropped into
~/.config/opencode/plugin/(only when opencode is installed).session.created→ idle,chat.message→ working,permission.asked→ blocked,session.idle→ done. -
codex
(experimental)
— codex's claude-shaped
hooks.jsonis JSON-merged into~/.codex/hooks.json(reversibly; uninstall prunes exactly our entries).SessionStart→ idle,UserPromptSubmit/PreToolUse→ working,Stop→ done. No blocked — codex's top-level hooks have no permission event. This replaced the oldernotifyoverride (which reported done only). -
antigravity
— the
agyCLI loads hooks from its shared~/.gemini/settings.json, so Thurbox JSON-merges its entries in without clobbering your settings.agyadopted claude's hook schema (verified against agy 1.0.9), so the mapping mirrors claude:SessionStart→ idle,PreToolUse→ working,Stop→ done, andNotification→ blocked (permission/approval prompts only). It has noUserPromptSubmit, so working is signaled at the first tool call. -
aider
—
--notifications-commandreports the only edge aider exposes: blocked (waiting for input). -
vibe
(experimental)
— Mistral Vibe loads hooks from
~/.vibe/hooks.toml; Thurbox drops a managed file in (only when vibe is installed). Verified against vibe 2.21.0:pre_tool→ working,post_agent→ done. No blocked — vibe's only hook types arepre_tool/post_tool/post_agent(no permission event), so a tool awaiting approval reads as working. If you already maintain your ownhooks.tomlthe write is refused (never clobbered) — vibe simply goes unreported. -
copilot
(experimental)
— GitHub Copilot CLI loads hooks from its own
~/.copilot/hooks/dir, so Thurbox drops a managed standalone file in (only when copilot is installed), never touching your other hook files.sessionStart→ idle,userPromptSubmitted/preToolUse→ working,agentStop→ done, andnotificationmatched topermission_prompt→ blocked. Bothbashandpowershellcommands ship, so status works on Windows too. -
pi
(experimental)
— the pi.dev CLI auto-discovers TypeScript extensions from
~/.pi/agent/extensions/, so Thurbox drops a managed one in (only when pi is installed).session_start→ idle,agent_start/tool_execution_start→ working,agent_end→ done, and a structuredask_user_questiontool call → blocked (pi has no permission hook, so a turn that ends by asking in prose signals done, not blocked). -
omp
(experimental)
— Oh My Pi is Pi-compatible and likewise auto-discovers TypeScript extensions,
from
~/.omp/agent/extensions/, so Thurbox drops a managed one in (only when omp is installed). It mirrors pi's extension but maps OMP's structured user-question tool — namedask— to blocked (it recognizes bothaskand pi'sask_user_question). Verified against OMP 17.0.6.
These use three manifest capabilities —
[[agent_patches]]
(append args to an agent),
[[external_files]]
(place a file in an agent's own config dir), and
[[config_merges]]
(reversibly deep-merge JSON into an agent's shared config). Each is fully reversible:
uninstall removes exactly what was added, matched by the
session signal
marker.
Where the config lives #
The wiring is applied
only to agents Thurbox launches
— it never edits your own global agent config (e.g. your personal
~/.claude/settings.json
). For claude the managed hooks file is passed with
--settings
, which claude
merges on top of
your own settings: inside a Thurbox session both your hooks and Thurbox's fire, while a
plain
claude
outside Thurbox sees only your own. The other agents are wired by a reversible merge
into — or a managed file dropped in — their own config dir.
| Agent | On-disk location | How it's applied |
|---|---|---|
| claude | ~/.config/thurbox/hooks/claude.json |
--settings
flag on the
claude
agent (claude merges it)
|
| aider | — (no file) |
--notifications-command
flag on the
aider
agent
|
| opencode | ~/.config/opencode/plugin/thurbox-status.js |
managed plugin file |
| codex | ~/.codex/hooks.json |
reversible JSON-merge of our entries |
| vibe | ~/.vibe/hooks.toml |
managed file (refused if you already have one) |
| copilot | ~/.copilot/hooks/thurbox-status.json |
managed standalone file |
| antigravity | ~/.gemini/settings.json |
reversible JSON-merge of our entries |
| pi | ~/.pi/agent/extensions/thurbox-status.ts |
managed extension file (refused if you already have one) |
| omp | ~/.omp/agent/extensions/thurbox-status.ts |
managed extension file (refused if you already have one) |
The home dir is
~/.config/thurbox/hooks
for a release build and
~/.config/thurbox-dev/hooks
for a dev build, so the two stay isolated.
Inspect or customize.
To see exactly what Thurbox installed, read the file for the agent above (e.g.
cat ~/.config/thurbox/hooks/claude.json
). The injected
--settings
/
--notifications-command
flags themselves live in the
claude
/
aider
entries of
~/.config/thurbox/agents.toml
. You can hand-edit a managed file, but self-heal rewrites it from the embedded payload
on the next TUI start / heartbeat tick — so to keep a change, either deactivate
the extension and wire the hook yourself, or edit the payload source under
extensions/hooks/
and reinstall.
Check that it is actually firing #
Every hook command ends in
|| true
on purpose — a missing
thurbox-cli
, a locked database or a hook firing outside a Thurbox session must never break your
agent. The cost is that a signal which never lands looks exactly like an agent that
simply has not signalled yet. To tell them apart:
thurbox-cli session doctor # every active session
thurbox-cli session doctor <uuid> # just one
It reports whether the extension is active, what this agent's hooks can report at all,
whether its payload is really on disk where the agent reads it, whether a hook command
could resolve
thurbox-cli
on
PATH
, what was last reported and how long ago, and whether the pane's foreground process
agrees with it. It exits non-zero when no state can reach Thurbox from a session, and it
only ever reads —
thurbox-cli extension reinstall hooks
is the repair.
A
--command
session is the one shape it will not fail: Thurbox never had an agent there to wire, so
it reports
no hooks expected
rather than broken wiring. If your driver started an agent inside one, say so —
see below.
Reporting state for an agent Thurbox did not launch #
Thurbox wires these hooks at launch, for an agent it knows from
agents.toml
. If your harness owns the agent launch itself — asking Thurbox for a bare
interactive shell and starting the agent inside that pane — no hooks are wired.
You can still report state, because
THURBOX_SESSION
is in the pane's environment and every child process inherits it:
thurbox-cli session signal --state working # identity from $THURBOX_SESSION
thurbox-cli session signal --state done
thurbox-cli session signal --state blocked --session <uuid> # from outside the pane
That is a supported integration point, not an internal detail: point your own agent's
lifecycle hooks at it and the session reports exactly like a built-in. Failing even
that, Thurbox reads the pane — a session that never signalled but whose foreground
process is an agent your
agents.toml
knows reports
state: "running"
with
state_source: "process"
, which is coarser than a hook by design but is not silence.
If what you launched in that pane
is
an agent Thurbox ships hooks for — you passed
thurbox-cli agent launch-args claude
through yourself, say — tell the row so:
thurbox-cli session create --name task-7 --command "$SHELL" --arg -i --reports-as claude
thurbox-cli session reports-as task-7 claude # or afterwards
thurbox-cli session reports-as task-7 --clear # take it back
The session is named after the command it launches, so without this
hook_coverage
,
hook_states_reportable
and
hook_blocked_is_heuristic
are all read against
bash
— and that last one answering
false
tells a supervisor the block signal is structured when it is really a text match on a
notification body. The declaration changes only what coverage is read against: the
launch is untouched, and
session restart
still replays the command you recorded.
Opt out #
Hooks is on without you asking. Turn it off (and back on):
thurbox-cli extension deactivate hooks # remove the wiring; won't come back
thurbox-cli extension activate hooks # re-enable itDeactivating records an opt-out flag, so startup self-heal won't resurrect it. All the per-agent wiring is reversible, so opting out leaves your agent configs exactly as they were.
Files #
See
extensions/hooks
for the
README.md
, the
extension.toml
manifest, and the per-agent payloads (
claude.json
,
codex-hooks.json
,
antigravity-hooks.json
,
opencode-status.js
,
vibe-hooks.toml
,
copilot-hooks.json
).