zirvDocs

DocsSupervision (zirv ctx)

Hook registration (Claude Code)

README.md, lines 6262–6508View source

Add to ~/.claude/settings.json:

json
{
  "hooks": {
    "Stop": [{ "hooks": [{ "type": "command", "command": "zirv ctx hook stop" }] }],
    "UserPromptSubmit": [{ "hooks": [{ "type": "command", "command": "zirv ctx hook prompt" }] }],
    "PreCompact": [{ "hooks": [{ "type": "command", "command": "zirv ctx hook pre-compact" }] }],
    "PreToolUse": [{
      "matcher": "Bash|PowerShell|Edit|Write|MultiEdit|NotebookEdit|Read|Grep|Glob|Agent|Task",
      "hooks": [{ "type": "command", "command": "zirv ctx hook pretool" }]
    }],
    "SessionStart": [{
      "matcher": "resume|clear",
      "hooks": [{ "type": "command", "command": "zirv ctx hook session-start" }]
    }],
    "SubagentStart": [{ "hooks": [{ "type": "command", "command": "zirv ctx hook subagent-start" }] }],
    "SubagentStop": [{ "hooks": [{ "type": "command", "command": "zirv ctx hook subagent-stop" }] }],
    "PostToolUseFailure": [{ "hooks": [{ "type": "command", "command": "zirv ctx hook tool-failure" }] }]
  }
}

SessionStart (issue #244) re-injects the latest stored handoff on resume/clear — a bare claude --resume or /clear sees it too, not only zirv ctx resume's own explicit flow.

Issue #769: a single PreToolUse entry now covers every tool name zirv's own guards care about, dispatching internally by tool_name — the command-safety check for Bash/PowerShell (previously its own separate zirv ctx safety check registration, matched on Bash|PowerShell alone), placeholder rehydration for shell/write/lookup tools, the orchestrator-write guard for file-modification tools, and the expensive-seat/skill-pointer guard for Agent|Task. zirv setup apply migrates an operator's own ~/.claude/settings.json away from the old, separately-registered safety hook automatically; zirv ctx safety check itself is unaffected as a standalone CLI command (-- <command>) and still self-suppresses if invoked as a leftover hook on a settings file that has not been migrated yet, so the two never evaluate the same tool call twice.

SubagentStop (issue #774) gates a native Task subagent's own final report against a few cheap, deterministic checks read from the SUBAGENT's own transcript — never the lead's — before it reaches the lead: a declared OUTPUT CONTRACT block with no fenced JSON reply at all, a claimed test run with no matching verification command anywhere in the transcript, and a bare BLOCKED report with no reason after it. The first violation found blocks with {"decision": "block", "reason": "..."}, capped at one block per subagent, ever, and fails open on any doubt (an unreadable transcript, stop_hook_active, or an unresolvable state dir). [subagent_stop_gate] enabled (default true) gates it, the identical narrow-only fold [missing_tests_gate] enabled uses below — a repository checkout may only turn it off, never force it on for an operator who disabled it.

SubagentStart (issue #832) records one agent-graph node (<state>/graph/ <session>/<agent_id>.json) with a single local file write; it prints nothing, exits 0 on any doubt and makes no network call. SubagentStop closes the node with its status, actual model and tokens read from the subagent's own transcript. zirv ctx graph [--json] [--events N] [--session ID] [--repo] (read-only) prints the agent tree merged from the session registry, delegations, work groups, subagent nodes and Codex sub-agent rollouts under ~/.codex/sessions (codex-cli 0.155/0.159 thread_spawn metadata), plus the newest N events merged from the decision, delegation, safety, Jev and proxy logs. Node files are capped per session and dropped a week after their session leaves the registry. Re-run zirv setup apply to install the SubagentStart hook; without it the graph still lists native agents, read from claude's own <session>/subagents/agent-<id>.meta.json files (a hook record wins on conflict). Each node carries job (first task line, redacted, 80 chars), workflow ({id, pack, step}), session (its root session) and steps (its last five tool calls, newest last, as {ts, tool, arg}; arg is a redacted one-line summary of at most 120 characters, and the list is always present, empty when nothing is readable). Steps are tail-read (at most 128 KB, cached on mtime and length) from a subagent's own subagents/agent-<id>.jsonl, a live Claude session's transcript, or a Codex rollout, in the same background gather as the rest of the graph. Every Agent dispatch appends one line to <state>/graph-dispatch/ so a nested agent finds its parent, and every dashboard pane or zirv agent run writes <state>/graph-launch/<session>.json so it keeps its parent, task and workflow step after its session is swept. Supervisor rulings appear as an architect event addressed to its session (the event kind keeps its old name). --session <id> and --repo scope the output; --json prints the newest 100 events unless --events is given.

PostToolUseFailure (issue #836, zirv ctx hook tool-failure) prints nothing and writes nothing unless [jev] retry is on and the TypeSafe credential is set; then it counts consecutive failures of one tool call shape in <state>/jev-retry/<session>.json (a successful PostToolUse clears it) and asks Jev once at the third. zirv setup apply registers the hook only while [jev] retry is on (and removes it when off), so enabling the key requires re-running zirv setup apply; with the key off no process spawns on a tool failure. [supervisor] enabled (#835) installs the same hook and shares the streak file (a zirv-launched Claude session also gets the Stop and tool-failure hooks in its launch settings, without zirv setup; zirv ctx status prints a supervisor: line with the harness and model, or off): at the third failure of a streak it also asks the supervisor for a retry-or-stop ruling, with or without [jev] retry.

Supervisor#

[supervisor] (issue #835, operator-only, off by default) is a strong model, such as claude-fable-5-1 or gpt-6-astra, that helps make decisions and steers the work. Its rulings are binding unless the operator overrides them, and they only ever narrow: a ruling can block a step, require a revision, stop a retry or pick one of the options the seat offered. It never writes code, answers a permission request, grants anything or widens scope.

Hooks never call a model and never wait. A trigger checks local state and spawns one detached zirv ctx supervisor consult (evidence on its stdin) that runs a single zirv ctx agent <harness> --mode read-only --quiet --json --task-class review delegation with -m <model> (the delegation ledger records the spend). The reply must match the kind's strict format; one that does not produces no ruling, a logged fallback, and today's behaviour (ask reports a redacted, 500-byte excerpt of the reply in its error). A choice reply may wrap its CHOICE: <n> line in markdown, a code fence or prose and may name the option by letter (B); two different picks are no ruling. A ruling is stored in <state>/supervisor/rulings/rulings.json (id, session, optional workflow, kind, verdict, reason redacted and capped at max_advice_bytes, ts, status open, resolved or overridden or lapsed) and mailed to the session (sender and topic supervisor). A consult's own child session never triggers (ZIRV_SUPERVISOR_CONSULT). model is required when enabled. There is no per-session cap on consults; each unit of work (a plan text, a diffstat) is consulted on once, and the helper's own token and tool-call budget bounds each consult.

KindReply formatWhere it bites
planAPPROVE or REVISE: <reasons>asked when zirv workflow advance completes the plan step; while a revise is open, advancing any later step is refused with the reason. Revising the plan artifact and re-advancing the plan step asks again, and the new ruling supersedes the old
doneDONE or NOT_DONE: <what is missing>asked at the Stop hook (once per distinct diffstat); while a not_done is open the Stop hook blocks with the reason, at most 3 times per ruling, and the workflow's final step is refused. A later done resolves it. Codex's Stop hook blocks the same way
retryRETRY or STOP: <reason>asked at the third failure of a tool-call streak, in place of the seat deciding; while a stop is open the tool-failure hook adds the ruling as context on each further failure, and a successful tool call ends it. It is advisory context, not a mechanical block (a hook cannot stop a tool), and a retry never lifts a Jev stop
choiceCHOICE: <n> and an optional REASON: linezirv ctx supervisor ask

zirv ctx supervisor ask "<question>" --option "<a>" --option "<b>" [--context-file F] [--timeout-secs 180] is synchronous and never runs from a hook: it prints the chosen option and its reason and records the ruling. With the supervisor on, the seat's injected instructions carry one line telling it to route real design or approach choices through ask and follow the ruling. Claude Code lifts the sandbox for an excluded command only when every command in the call is excluded, so ask must be its own Bash call; on a sandbox denial it says to run it with the sandbox disabled. zirv ctx supervisor override <id> [--reason ...] marks a ruling overridden and is operator-only: it is refused inside an agent session (a session, socket or seat-role variable present, even if empty) and unless stdin and stdout are terminals, and it is never auto-allowed, so an agent's attempt surfaces as a permission prompt (ask and status stay auto-allowed). The residual: the ruling store lives in the state directory, which sessions can write, so binding rulings guard against a seat that drifts, not against a hostile agent. A not_done or revise ruling whose superseding consult cannot run lapses instead of binding forever when there is nothing to review, and only when git status --porcelain succeeds and is empty. The Stop hook blocks on a not_done ruling at most 3 times. The lapse is logged and mailed, so ACTIVITY shows it. Deliberate evasion of the prompt (for example env -u ... script -q /dev/null ...) is part of the same residual. zirv ctx supervisor status [--json] [--session <short>] prints per-session state and the open rulings. Re-run zirv setup apply after enabling.

The Stop hook forwards verdicts to a supervising wrap or exec when one owns the session, and otherwise prints a non-blocking advisory. It exits 0 even when it is invoked wrongly, because Claude Code reads a Stop hook's exit 2 as "block the stop" -- a real block instead uses the JSON {"decision": "block", "reason": "..."} envelope on stdout with exit 0.

The one case where the Stop hook actually blocks is the missing-tests gate ([missing_tests_gate], default enabled = true): for a HEADLESS Worker/ Single session (ZIRV_CTX_HEADLESS=1 -- an interactive session is never affected) that edited or created a non-test source file this turn but touched no test file for the change (a tests/test path component, a test_*/*_test.*/*.test.* filename, or -- Rust only -- a change on or after a file's own #[cfg(test)] line), it blocks once with a reason asking for a focused test of the behaviour change, including the invalid-input path, then a test-suite run. It never blocks a second time in the same session (a persisted per-session marker, independent of stop_hook_active), never fires when stop_hook_active is already true, and fails open -- like every other Stop-hook check -- on any doubt at all (an unreadable repo, no git, a config load failure). A repository checkout may only turn it off ([missing_tests_gate] enabled = false in <repo>/.zirv/ctx.toml), never force it on for an operator who disabled it.

jev.missing_tests (off by default) softens that block with one Jev call instead of removing it: right before the deterministic gate above would block, it asks Jev a single metadata-only question built from local numeric facts alone (never a path, filename, or file content) — how many non-test source files changed, a changed-lines size bucket, whether the repo has any test files at all, how many existing test files already mention a changed module's own name, and the change's doc-only share — "is a new test owed for this change?". Only a decisive, strongly "not owed" answer (at or below Jev's own skip threshold) skips that one block, and skipping never persists it as blocked, so a later, still-test-less turn in the same session can still be asked or blocked. Anything else — the gate off, no credential, an indecisive or error answer, or a decisive answer that isn't strongly "not owed" — blocks exactly as the deterministic gate already does.

Scope guard ([scope_guard], default enabled = true) is a scope-creep guard, not a correctness check: it never decides whether an edit is right, only makes the request's own preservation language visible at the moment of editing and catches an unrequested-fix claim once at the end. UserPromptSubmit splits the prompt into sentences and keeps up to three (capped at 400 characters joined) that use preservation/limitation language -- "same as always/before", "works the same", "as before", "exactly as", "unchanged", "keep ", "preserve", "don't/do not/never change/touch/modify/alter", "only ", "backward(s) compat", "existing behavio(u)r", "leave ... alone" -- plus whether the request itself already asks for a fix (fix/bug/broken/error/wrong/crash/regression/off-by-one wording), into a small per-session record; a new prompt replaces the record. PreToolUse shows a non-blocking checkpoint (additionalContext, never a permission change) on the first Edit/MultiEdit/NotebookEdit call, or a Write to a file that already exists, after each new prompt: it quotes the extracted constraints (when there are any) and asks the agent to check the edit is actually needed for what was asked, rather than an unrequested fix or improvement -- interactively, "ask the user first"; headlessly (permission_mode == "dontAsk"), "leave it and list it under 'Found, not changed' in your final report" instead, since there is no one to ask. It rides in the same additionalContext envelope as the orchestrator-write advisory and the reuse probe rather than replacing either. The same one-time checkpoint also fires from PostToolUse after the first Bash/PowerShell command that changes an existing tracked file, worded for a change that already happened, for a headless agent that edits through the shell and never touches Edit/Write at all. The same checkpoint also folds in a "tests owed" line ("Write a focused test for each behaviour change in this same pass -- the run cannot finish without one. Extend the existing test file or class ...") whenever the missing-tests gate below is enabled, the session is headless, and the file being changed is a non-test, non-doc source file -- fired at the FIRST edit rather than waiting for the missing-tests Stop gate to say it after the whole turn is already done. This line can fire the checkpoint on its own even with [scope_guard] enabled = false. UserPromptSubmit also extracts up to eight stated, checkable details -- a backtick- or double-quoted literal, or an ordering/format/ exactness word ("sorted", "order", "ascending", "descending", "exactly", "exact", "format", "exit status"/"exit code", "stderr", "stdout", "print(s)", "message", "case-insensitive", "comma-separated", "no spaces", "trailing", "leading"), never duplicating a sentence already captured as a constraint -- and the checkpoint appends them as a numbered "Stated details to check before you finish: (1) ... (2) ..." list, governed by [scope_guard] enabled like the constraints themselves. Stop is a backstop, run only after every other Stop gate/backstop above already had its chance to block (at most one block per Stop): when the request did NOT itself ask for a fix and the closing report's own wording claims one anyway (a fix verb alongside a bug word in the same or the next sentence, "also fixed/changed/updated/refactored", or "while (I was) at it/there/here"), it blocks once, quoting the offending sentence and the request's own constraints, asking the agent to revert the change and report it as "found, not changed" unless it was strictly required -- interactively, or ask the user first. It never fires twice for the same prompt, never fires when stop_hook_active is already true, and fails open on any doubt at all (no session identity, no recorded prompt, an unreadable transcript). A repository checkout may only turn it off ([scope_guard] enabled = false in <repo>/.zirv/ctx.toml, or the operator's own ZIRV_CTX_SCOPE_GUARD_ENABLED), never force it on for an operator who disabled it; disabled means no record is ever written, no checkpoint is ever shown, and Stop never blocks for it.

Shell edit guard (opt-in: [edit_guard], default enabled = false, env ZIRV_CTX_EDIT_GUARD_ENABLED; a repo's .zirv/ctx.toml may only disable it; zirv-supervised sessions only, i.e. those with ZIRV_CTX_SESSION set): when enabled, PreToolUse denies a Bash command that runs an inline interpreter script (python/python3/py/node/ruby/perl with a heredoc, -c or -e) containing a file-write call when one of its quoted path literals is a git-tracked file, and points the agent at the Edit tool instead (scripted string splices break escapes and indentation). Each distinct command is denied at most once per session: re-issuing the identical command is allowed, so a false positive costs one retry. A safety deny/ask still wins, a denial is logged as hook/deny/edit-guard, and any doubt (not a git repo, git missing or timing out, unreadable state) allows silently.

The Stop hook is also how a supervisor learns which file the agent is writing: the agent mints its own session id, so the transcript path travels on the turn signal the hook sends. Register it, or wrap has nothing to verify a compaction against and no context to distil a restart handoff from.

Current Codex versions support lifecycle hooks with the same JSON event shape. zirv setup apply merges Zirv's handlers into ~/.codex/hooks.json; review and trust new definitions with /hooks in Codex. The older zirv ctx hook notify compatibility entry point remains available for Codex versions configured with the external notify program. Rollout event parsing is still tracked in issue #11.