Agent hooks

The built-in hooks extension wires each coding agent's lifecycle hooks to friring-cli session signal so every session reports its state back to Friring. 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 Friring instead of being fetched).

Overview #

The hook command is always friring-cli session signal --state <working|blocked|done|idle> , suffixed || true so it can never break the agent. Identity comes from the injected $FRIRING_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.

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 — Friring 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, and Notification → 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.json is 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 older notify override (which reported done only).
  • antigravity — the agy CLI loads hooks from its shared ~/.gemini/settings.json , so Friring JSON-merges its entries in without clobbering your settings. agy adopted claude's hook schema (verified against agy 1.0.9), so the mapping mirrors claude: SessionStart → idle, PreToolUse → working, Stop → done, and Notification → blocked (permission/approval prompts only). It has no UserPromptSubmit , so working is signaled at the first tool call.
  • aider — --notifications-command reports the only edge aider exposes: blocked (waiting for input).
  • vibe (experimental) — Mistral Vibe loads hooks from ~/.vibe/hooks.toml ; Friring 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 are pre_tool / post_tool / post_agent (no permission event), so a tool awaiting approval reads as working. If you already maintain your own hooks.toml the write is refused (never clobbered) — vibe simply goes unreported.
  • pi (experimental) — the pi.dev CLI auto-discovers TypeScript extensions from ~/.pi/agent/extensions/ , so Friring drops a managed one in (only when pi is installed). session_start → idle, agent_start / tool_execution_start → working, agent_end → done, and a structured ask_user_question tool 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/ . Same events as pi, except its structured user-question tool is named ask , so blocked fires on either ask or ask_user_question .
  • copilot (experimental) — GitHub Copilot CLI loads hooks from its own ~/.copilot/hooks/ dir, so Friring drops a managed standalone file in (only when copilot is installed), never touching your other hook files. sessionStart → idle, userPromptSubmitted / preToolUse → working, agentStop → done, and notification matched to permission_prompt → blocked. Both bash and powershell commands ship, so status works on Windows too.

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 Friring 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 Friring session both your hooks and Friring's fire, while a plain claude outside Friring 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/friring/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/friring-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/friring-status.json managed standalone file
antigravity ~/.gemini/settings.json reversible JSON-merge of our entries
pi ~/.pi/agent/extensions/friring-status.ts managed extension file (refused if you already have one)
omp ~/.omp/agent/extensions/friring-status.ts managed extension file (refused if you already have one)

The home dir is ~/.config/friring/hooks for a release build and ~/.config/friring-dev/hooks for a dev build, so the two stay isolated.

Inspect or customize. To see exactly what Friring installed, read the file for the agent above (e.g. cat ~/.config/friring/hooks/claude.json ). The injected --settings / --notifications-command flags themselves live in the claude / aider entries of ~/.config/friring/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.

Opt out #

Hooks is the one extension that's on without you asking. Turn it off (and back on):

bash
friring-cli extension deactivate hooks   # remove the wiring; won't come back
friring-cli extension activate hooks     # re-enable it

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