DocsSupervision (zirv ctx)
Hook registration (Claude Code)
Add to ~/.claude/settings.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.
| Kind | Reply format | Where it bites |
|---|---|---|
| plan | APPROVE 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 |
| done | DONE 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 |
| retry | RETRY 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 |
| choice | CHOICE: <n> and an optional REASON: line | zirv 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.