Hooks Reference
A hook is a shell command Claude Code runs at a lifecycle event. ai-core-kit
ships exactly one hook, the contract-gate, as CHILD payload: a PreToolUse
guard rendered into a fork only when the archetype carries it and the master
switch is on. It is the CHILD-layer expression of the kit’s
design-contract-first methodology. Full behavioral spec:
Design-Contract Gate.
contract_gate.protected_paths/scope, consults the approved-contract oracle, and acts per contract_gate.mode. A missing/unparseable manifest fails open as off.
The one hook: contract-gate
| Field | Value |
|---|---|
| Kind | hook |
| Layer | CHILD |
| Event | PreToolUse |
| Matcher | Edit|Write|MultiEdit|NotebookEdit |
| Command | python3 ${CLAUDE_PROJECT_DIR}/.claude/hooks/contract-gate |
| Rendered into | backend-api and fullstack archetypes |
| Master switch | features.sdd_gate: true (false → the hook and matcher are not rendered at all) |
| Path | templates/archetypes/<archetype>/.claude/hooks/contract-gate |
A PreToolUse matcher can filter only by tool name, never by file path. So
the hook receives every Edit/Write/MultiEdit/NotebookEdit call and reads
tool_input.file_path from stdin JSON to scope in-script. Runtime is pinned
to python3; the glob dialect is fnmatch with **.
The three modes
contract_gate.mode is a required enum. The modes are three distinct exit-code
mechanisms — a load-bearing distinction, since the wrong exit form silently
no-ops the guard:
| Mode | Exit | Output | Effect |
|---|---|---|---|
block | 2 | sets hookSpecificOutput.permissionDecision: "deny" | Stops the tool call. |
warn | 0 | message on stderr / additionalContext | Logs and continues — does not block. |
off | 0 | none; early-return before parsing the manifest body | No-op. |
The
blockform is the footgun: it requiresexit 2ANDhookSpecificOutput.permissionDecision: "deny"— not a top-leveldecisionkey, and notexit 1. Get it wrong and the guard fails open.
In-script scoping and precedence
Three glob lists in contract_gate drive scoping, with fixed precedence:
exemptwins over everything — an edit matching it is always allowed.protected_paths(required,minItems 1, so the gate can never be vacuous): edits here require an approved contract.scope(optional; defaults toprotected_paths): the subset that also requires an approved contract.- A file matching none of the above is ALLOWED.
The “approved?” oracle is contracts[]: an edit under a protected path is
allowed only if some contracts[] entry whose scope covers it has
status: approved (a draft does not unlock its scope). Per-archetype
protected_paths defaults are resolved by /ack-init from the chosen archetype
and frozen into the manifest:
| Archetype | Default protected paths |
|---|---|
backend-api | src/**, migrations/**, openapi/** |
fullstack | app/**, api/**, src/** |
infra-iac | supplied via the manifest field (no hardcoded src/**, which would be vacuous for infra) |
Fail-open / fail-closed contract
- Fail-open at runtime. If the manifest is missing or unparseable when the
hook runs, the gate behaves as
offplus a stderr notice — it never exits2and never wedges the session. - Fail-closed at author-time.
/ack-initvalidates the manifest against the frozen schema before rendering; an invalidcontract_gateblock aborts init.
Shipped vs. roadmap
| Piece | Status |
|---|---|
contract_gate.* and contracts[] manifest fields (frozen schema) | Shipped |
settings.json matcher + hook location + fail-open stub | Shipped |
Per-archetype protected_paths defaults | Shipped (interview/schema) |
| Deep runtime: fnmatch scoping, approved-oracle walk, 3 mode mechanisms | P5 / roadmap |
See also: Design-Contract Gate (full spec),
Manifest & Interview for the contract_gate
and contracts[] fields the hook reads.