Quick answer: Claude Code hooks are deterministic handlers at fixed lifecycle points. PreToolUse is the only hook that can stop a tool before it runs; exit code 2 blocks the call and sends stderr to Claude. Since v2.1.295, onFailure: "block" also blocks when the hook crashes, times out, or returns an unexpected code. Without it, your policy is fail-open.
The most consequential Claude Code release this month is one config line. Not a model, not a context window. onFailure: "block" changes what happens when the deterministic script between the agent and your filesystem fails. Before v2.1.295, that failure let the action through.
That is the difference between governance and theater. The model is probabilistic; a hook is not. Every rule about what an agent may touch belongs in the deterministic layer, because a prompt cannot enforce it.
What changed in October
onFailure makes failure a policy decision
A command or HTTP hook can fail before your policy runs: it cannot start, times out, or exits unexpectedly. The default was to continue. v2.1.295 (8 October) added onFailure: "block", so those failures stop the action (changelog). It does not apply to async hooks — the right design, since async hooks are for logging, not gates. At release, Anthropic's reference still described the old fail-open behavior for timeouts; version-gate the setting.
Mods move hooks in-process
v2.1.287 introduced Claude Mods, plugins that modify deeper behavior through in-process hooks. A mod exports register(on, options) and attaches handlers with on("event", handler); handlers can observe, rewrite, or answer with a denial (Mods reference). Events include prompt.submit, tool.call, agent.spawn, and since v2.1.300 prompt.change. agent.spawn can refuse a subagent or rewrite its model and prompt — a real control point for cost and blast radius.
The unattended run gets a watchdog
v2.1.301 (15 October) added CLAUDE_CODE_RETRY_WATCHDOG_MAX_USAGE_LIMIT_WAIT_MS, so unattended retry mode ends a request when a usage limit resets too far away instead of sleeping until it does. An agent at 3 a.m. needs both layers: hooks decide what it may do; the watchdog decides when the attempt is over.
Why fail-open hooks are governance theater
Exit codes are the contract
Exit 0 is silent success; Claude does not see stdout. Exit 2 is a blocking error, and stderr goes to Claude. Any other code is non-blocking and shown only to the user (hooks reference). A hook that detects a violation, prints a warning, and exits 0 has not governed anything. The block is exit 2 plus a reason the model can act on.
PreToolUse can also return JSON with permissionDecision (allow, deny, or ask), permissionDecisionReason, and updatedInput; additionalContext works on PostToolUse and UserPromptSubmit, not here.
The failures nobody tests
A hook can fail to launch (a path typo, a missing interpreter), time out, or exit unexpectedly (a missing dependency). Without onFailure, all three allow the action. With it, all three block it. A failure that blocks every call is an outage; one that allows every call is a breach. Keep the gate small, dependency-light, and observable.
There is also an exit-code collision. Issue #80697 reports that a hook which cannot launch may be read as a deliberate block — CPython exits 2 when it cannot open a script, the code reserved for "deny". The reverse has been reported too (#43407). Verify the gate on your version.
Where each control belongs
Tool boundary: PreToolUse
This is the enforcement point. Match state-changing tools — Bash, Edit, Write, and production MCP tools — and keep the rule set small. Start with recursive deletes outside the workspace, force-pushes, piping a download into a shell, credentials in arguments, and writes to .git/hooks, CI config, or deployment manifests. Use updatedInput sparingly; a rewrite is harder to audit than a denial.
Turn boundary: Stop and SubagentStop
A Stop hook can refuse to let the agent end its turn. Exit 2 with "tests failed" and Claude keeps working. This is where a definition-of-done gate belongs: typecheck, lint, the relevant test, and a check that the diff touches only allowed paths.
Do not let the agent grade its own homework. TestSuiteBench tested agents on 165 applications with roughly 74 mutants each. Their generated suites passed 88% of their own tests, yet mutation scores were below 60% — missing about 40% of specification-derived faults. Green tests prove the agent can write tests, not that the change is correct.
Session and evidence boundaries
SessionStart should record the working directory, CLI version, loaded settings, and a hash of the hook configuration; a changed hash is a review event. Refuse project hooks in repositories you do not trust.
PostToolUse runs after a successful call and cannot block it. Use it for formatting, diff capture, and SIEM events; PostToolUseFailure is separate. Log the rule, tool, input, and policy version for every block — without the last one, you cannot tell a regression from a deliberate change.
A fail-closed reference implementation
One gate on Bash, one done-gate on Stop, both fail-closed and time-bounded:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/pre-tool-guard.sh",
"timeout": 10,
"onFailure": "block"
}
]
}
],
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/done-gate.sh",
"timeout": 120,
"onFailure": "block"
}
]
}
]
}
}The guard script fails closed at every stage, not just at the policy check:
#!/usr/bin/env bash
# .claude/hooks/pre-tool-guard.sh
set -uo pipefail
payload=$(cat)
command -v jq >/dev/null 2>&1 || {
echo "Hook dependency missing: jq. Blocking by default." >&2
exit 2
}
cmd=$(printf '%s' "$payload" | jq -r '.tool_input.command // empty') || {
echo "Hook could not parse tool input. Blocking by default." >&2
exit 2
}
[ -z "$cmd" ] && exit 0
deny() {
echo "$1" >&2
jq -cn --arg reason "$1" \
'{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$reason}}'
exit 2
}
case "$cmd" in
*"rm -rf /"*|*"rm -rf ~"*) deny "Recursive delete of / or home is blocked." ;;
*"git push --force"*|*"git push -f"*) deny "Force-push requires a human." ;;
*"curl"*"| sh"*|*"curl"*"| bash"*) deny "Piping a download into a shell is blocked." ;;
esac
exit 0The dependency check runs before the policy check, so a missing jq cannot allow everything. Every denial carries a reason in stderr and JSON, so the model can explain it and the log can group it. Add separate Edit/Write matchers with a path allowlist.
The security debt hooks do not solve
Project hooks are executable code
A repository can ship .claude/settings.json with a SessionStart hook; clone it, start Claude Code, and that command runs with your privileges. Check Point Research documented this class of issue and assigned CVE-2025-59536 and CVE-2026-21852; the reported issues were patched (Check Point).
The documentation gap matters more. An August 2026 docs issue asked whether project hooks are gated by workspace trust, and what happens under claude -p; the reference did not say, and a feature request reported repo-local hooks running without approval. Review the file and keep project hooks off in untrusted repositories.
Hooks are not a sandbox
A hook runs with your environment: the same files, ~/.ssh, cloud credentials. Blocking Bash does not block an MCP tool or a subagent with its own permissions. Run untrusted work in a container, scope credentials, and pin the CLI: the hook surface changed at least four times in October. For the wider threat model, see the security model.
A 30-day rollout plan
Week 1 — inventory and shadow mode. List every hook, with its event, matcher, timeout, and failure behavior. Run new rules in log-only mode and compare would-deny decisions with what happened.
Week 2 — enforce two rules. Choose the two failures that would hurt most: destructive filesystem operations and credential exfiltration. Add onFailure: "block", a ten-second timeout, and a reason for every denial. A noisy gate gets disabled, and a disabled gate looks like coverage.
Weeks 3–4 — prove the gate. Replay fixtures, including a missing dependency, malformed JSON, and a slow check. Add the Stop done-gate and test it against a failing branch. Track false positives, time to unblock, and escaped incidents before expanding to Edit/Write rules. The metric is not hook count; it is the share of state-changing actions behind a deterministic gate.
FAQ
What are Claude Code hooks?
Deterministic handlers at lifecycle points such as session start, around tool calls, and when a turn ends. They can be shell commands, HTTP endpoints, MCP tools, prompts, or in-process Mods.
What does onFailure: "block" do?
It makes a command or HTTP hook that cannot start, times out, or exits unexpectedly block the action. Added in v2.1.295; it does not apply to async hooks.
Do hooks replace permissions or sandboxing?
No. Hooks are policy, not isolation. They run with your privileges and cannot contain a compromised tool or plugin. Pair them with least-privilege credentials, workspace trust, and a container.
Why did my PreToolUse hook not block the call?
It exited 0 instead of 2; the matcher missed the tool; it was async or on PostToolUse, which cannot block; or it never launched and your version is fail-open.
Are project-level hooks safe?
Treat .claude/settings.json as executable code. A cloned repository can define hooks that run on session start with your credentials. Review it before opening an untrusted project.
The control plane you actually own
October's releases were about reliability, but the durable lesson is control. The one layer you can specify, test, and audit is the deterministic one.
Make every gate fail closed. Make every denial explain itself. Then let the agent run unattended — because you finally have something stronger than confidence.
For the cost side, see the cheap-subagent playbook; for the attack surface, see AI coding agent supply-chain attacks; for the rest of the October wave, see the release roundup; and for the harness, see the unbundled coding agent harness.
Sources
- Claude Code changelog — v2.1.287, v2.1.294, v2.1.295, v2.1.300, v2.1.301, v2.1.303, 18 October 2026.
- Hooks reference, Mods reference, React to events.
- Check Point Research — CVE-2025-59536 and CVE-2026-21852.
- Issue #80697, #43407, #83502, #62519.
- TestSuiteBench, 9 October 2026.
Related reading
The Cheap-Subagent Architecture: Why AI Agent Orchestration Wins in 2026
Haiku 5.5 costs one-fortieth of Opus per input token, Claude Code now gives each subagent its own effort level and token budget, and Codex runs read-only tools in parallel. But naive multi-agent teams cost 1.8 to 7 times more per task. Here is the architecture that captures the savings without buying the chaos.
Is AI Actually Making Developers Faster? What the 2026 Evidence Says
Ask a developer if AI makes them faster and the answer is instant and large. Measure it and the number shrinks, sometimes reverses. Here is what 30,000 developers report, what the studies can actually show, and how to measure the effect on your own team.
How AI Coding Agents Got Hacked: The 2026 CI/CD Supply-Chain Reckoning
In 2026 the attack surface moved from your editor to your issue tracker. A malicious GitHub issue title backdoored an npm package, three major agents were shown to treat PR text as commands, and trusted PyPI libraries shipped credential-harvesting malware. Here is what happened and five defenses that hold.