A policy error is a policy decision.
Claude Code 2.1.295 added onFailure: "block" for command and HTTP hooks. The release note says the action is blocked when the hook cannot start, times out, or exits with an unexpected code.[1]
That setting closes a gap between a guard saying "deny" and a guard failing to answer. Those states need different records. A denial means the policy ran and rejected the action. A timeout means the policy result is unknown.
Silence from the guard is not permission.
Exit codes are part of the contract.
The current hook reference says exit code 2 is the blocking signal for most events that can block. Exit code 1 is normally a non-blocking error when there is no valid JSON decision, even though Unix tools often use 1 for failure.[2]
Do not treat every nonzero code as equivalent. Write down which code means deny, which codes mean the checker broke, and how malformed JSON is handled. Include stderr in the receipt, but do not confuse an error message with an enforced stop.
Put failure cases in the fixture.
Anthropic describes hooks as deterministic controls that run at named points in the session lifecycle. The guide lists command validation, protected-file checks, notifications, and project-rule enforcement as hook jobs.[3]
A useful fixture covers more than one expected denial. Make the hook return allow, return a documented block, crash before output, exceed its time budget, and emit invalid output. Assert both sides of each case: whether the action ran and what the operator saw.
Keep the outer boundary.
A hook runs inside a larger execution system. OpenAI's Codex 0.162.0 notes include fixes for multiple denied files, writable sandbox-construction executables, and ripgrep configuration that could weaken deny globs.[4] Those fixes concern a different tool, but they carry the same engineering lesson: policy text and enforcement code can drift.
Use hooks for a fast, visible decision. Keep file permissions, sandbox policy, credentials, network rules, and human approval outside the hook. Then test the hook and the outer boundary separately. One broken checker should not open every route.