Expand description
probe-hook — the edit guard as a SHIPPED envelope (the eleventh ask).
Agenda::edit_guard and GuardVoices::for_edit are mutation-tested and
register-driven — and before this crate, every consumer wrapped them in the same
four pieces of unjudged glue: a bash wrapper, inline JSON-parsing Python, a
build-on-demand fallback that could run a stale binary, and hand-authored
settings.json plumbing. All of it outside the mutation boundary, each consumer
with different bugs. This crate is that envelope, inside the boundary:
- speaks the Claude Code hook protocol natively — reads the PostToolUse JSON
from stdin, extracts
tool_input.file_path, honoursCLAUDE_PROJECT_DIR; - discovers the repo’s own declarations — voices derived from the tree
(
GuardVoices::for_edit), classes taught fromspec/agenda.register; the consumer writes zero code; - carries the fail-open contract inside the boundary — every internal failure
(malformed JSON, missing path, unreadable file, refused register) is silence,
as a drilled property of
respond, not a|| exit 0convention; - installs its own wiring —
installwrites or idempotently merges thesettings.jsonentry, so the plumbing is derived output, never hand-authored.
Honest frame — version skew: a globally installed probe-hook can be newer or
older than the probe-algebra a repo pins, and the two can disagree about register
grammar or voice derivation. The floor shipped here: every non-silent voice block
carries the binary’s version on its last line, and the guard is advisory and
fail-open, so skew degrades to weaker advice, never a false refusal. (Re-execing a
repo-local build is the known nicer form; deliberately not built until skew is
observed hurting.)
Functions§
- extract_
path - The edited file’s path, out of the hook protocol’s stdin JSON
(
tool_input.file_path).Nonefor anything else — malformed JSON, a tool event without a path — because the hook’s silence must be total on input it does not understand. - install
- Write or merge the
.claude/settings.jsonhook entry — the plumbing as DERIVED output. Idempotent: an entry whose command already invokesprobe-hookis left alone; everything else in the file is preserved untouched. Returns a line saying what happened; errs only on asettings.jsonthat exists but does not parse (never overwrite what cannot be read — that file is not ours). - respond
- The whole envelope, total: hook JSON in, at most one voice block out. Every
failure path is
None— the fail-open contract as a return type. The voices are derived fromproject_dir(the register, the shim evidence), the classes come fromspec/agenda.register, and the edited path is normalized repo-relative so messages match what the repo’s own gates would say.