Claude Code Hooks Fail Open by Default: What onFailure "block" Fixes and What It Misses
A guard hook protects you only on the days it runs. Claude Code v2.1.295 added a per-hook switch that blocks when the guard breaks, but you have to set it, and it cannot catch a script that runs and says all is well.
A guard hook protects you only on the days it runs. Claude Code v2.1.295 added a per-hook switch that blocks when the guard breaks, but you have to set it, and it cannot catch a script that runs and says all is well.
A Claude Code guard hook with a wrong script path let the command it guarded run.
So did one that slept 30 seconds against a 3-second timeout.
Claude Code hooks fail open by default.
A command or HTTP hook may fail to start, time out or exit with an unexpected code. On most events, Claude Code still carries out the action. Since v2.1.295, you can set onFailure: "block" on that hook to stop the action instead.
The setting stays off until you add it.
One tester ran both cases on Claude Code v2.1.295. The request was to run echo HOOK_TEST_RAN, with echo pre-allowed.
The tester's note on both runs: "the final answer gave no sign that the guard had not run".
What counts as a failed guard?
A hook has three ways to answer. Exit 0 is success, and the action goes ahead unless the hook prints a decision that blocks it.
Exit 2 is a blocking error. On events that can block, it stops the action. Anything else fails without blocking.
The hooks reference describes that last case as a hook which "exits with any other code, or fails in some other way, such as not starting or printing invalid JSON. The action goes ahead."
Exit 1 is the usual Unix code for failure. Claude Code still lets the action go ahead.
The same page states what that means for a guard: "a policy hook with a wrong path or a crashing script lets everything through."
PreToolUse hooks run before a tool call. A timed-out one stops nothing either: the call moves on to the normal permission flow, so your allow, ask and deny rules still apply.
Only exit 2 blocks on its own; every other failure follows the onFailure setting, which is "continue" unless you set "block".What did v2.1.295 change?
One field.
The release notes say it blocks "a hook that can't start, times out, or exits with an unexpected code". The reference gives the field two values, "continue" and "block", and names "continue" the default.
So nothing changed for the guards you already have. A reporter at MIXED read both Anthropic pages on release day. The reporter put it this way: a hook written last month "behaves on 2.1.295 the way it behaved on 2.1.294 unless its author goes back and adds the line".
The docs lagged the release. At 21:04 UTC on October 8, neither the hooks reference nor the hooks guide mentioned onFailure. The reference still said a timed-out hook does not block. The reference now documents the field.
A second tester, at Classmethod, wired up a hook on UserPromptSubmit, which runs when you submit a prompt, before Claude processes it. The hook only runs exit 1. Without onFailure, the prompt reached Claude and an answer came back.
"onFailure": "block" stopped the prompt.
Both runs of claude -p exited 0. That is Claude Code's non-interactive mode, the one scripts use.
A blocked prompt still exits 0, so a wrapper script cannot use the exit code to learn that a hook blocked it. It has to read the output.
Which hooks and failures does onFailure cover?
Anthropic's release notes name two hook types: command and HTTP. They document the switch for those two only.
Prompt and agent hooks ask a model to decide, so the answer is a judgement. The v2.1.294 release fixed ones written as instructions, such as "Block commands that...", for "allowing what they should block". The notes document no switch for them, so do not use one as your gate.
For a command hook, the failures are a wrong path, a crash, an exit code other than 0 or 2, and a timeout.
The default timeout is 600 seconds for command and HTTP hooks. It is 30 on UserPromptSubmit (hooks docs). A hung guard can hold a call for up to 600 seconds, so a short timeout matters as much as onFailure. A timed-out UserPromptSubmit hook still lets the prompt reach Claude, and Claude Code discards its output. onFailure: "block" blocks the prompt instead.
HTTP hooks fail in their own way. A non-2xx status or a failed connection is a non-blocking error until you set onFailure: "block".
Table: What happens when a guard breaks
| What breaks | Default, continue | With onFailure: "block" |
|---|---|---|
| The script path is wrong | Runs anyway | Blocked |
The script hangs past its timeout | Command runs | Blocked |
| The script exits 1 | The prompt goes through | Blocked |
| The script exits 0 by mistake | The command goes ahead | The command goes ahead |
A Windows powershell.exe -File script is missing | Runs anyway | The command runs, per an open issue |
Each row cites a tester, an open issue report, or Anthropic's own docs. The last two rows are the failures the switch does not catch.
Should every guard fail closed?
No.
A guard that fails closed blocks the action whenever the guard itself breaks. Its own typo becomes an outage.
Some people who run guards chose to fail open on purpose.
GitGuardian built the agent hooks for ggshield to fail open. When it cannot scan, it "does not block the action" and shows a warning. The design "keeps your AI coding tool usable when ggshield is misconfigured".
ggshield pairs that open default with a check. ggshield machine doctor reports whether each agent's hook is installed and whether your token can reach GitGuardian.
One practitioner keeps his guards open. They protect him "from my own automation moving quickly, not from an adversary".
He asks one question of any guard: "if this hook breaks, would I rather lose the work or lose the protection?"
He names the other half. A guard "standing between an attacker and your credentials should fail closed".
By that question, a guard on deletes, force pushes, secret reads or outbound calls costs more when it slips than when it blocks, so it should close.
A formatter or a notifier should stay open. Its failure should show up somewhere you look, and the last step below shows one way.
What can onFailure not catch?
A script that runs and says all is well.
The Developers Digest tester ran a naive guard on a machine without jq. The script read an empty command. Its pattern matched nothing. It exited 0.
The tester's verdict: "onFailure: "block" would not save you here either, because from the agent's point of view the hook succeeded."
An open Anthropic issue shows the same hole on Windows. A hook can run powershell.exe -File on a missing script and exit 0xFFFD0000. Claude Code logs that run as a success, so onFailure: "block" lets the call go ahead, but the same hook does block on exit 1. The reporter opened the issue on October 9 and saw it again on 2.1.296. It was still open on October 14.
Anthropic's v2.1.295 notes record one more hole, now closed. A mod is a plugin that registers hook functions Claude Code runs in its own process. Claude Code handed one a deeply nested tool input cut short with no error, "so a guard could pass content it never saw".
So the script has to turn its own errors into exit 2.
The Developers Digest tester's working version starts with set -euo pipefail and sets a trap that runs echo "guard.sh crashed on line $LINENO, blocking to be safe" >&2; exit 2. It blocked on a payload that was not JSON and on a PATH with no jq.
Some rules need no script at all. Claude Code checks deny rules first, then ask, then allow.
A hook that returns allow does not override a deny rule: "Deny and ask rules are still evaluated regardless of what the hook returns", says the hooks docs. A deny rule has no script to break.
How do you test your own guards?
- Find every hook that can block. Look for hooks on events where exit 2 blocks, such as
PreToolUseandUserPromptSubmit, in.claude/settings.jsonand~/.claude/settings.json. The section "Exit code 2 behavior per event" in Anthropic's reference lists which events can. Then runclaude --versionto confirm v2.1.295 or later. - Add
onFailureto each guard that stands between a model and something you cannot undo. This is Anthropic's example, aPreToolUsehook on Bash:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "node",
"args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/check-command.js"],
"onFailure": "block"
}
]
}
]
}
}
- Add a short
timeout, in seconds, to the same hook. The Developers Digest example uses 5. - Break each guard on purpose. Anthropic's own test is to leave
check-command.jsmissing and ask Claude to run a Bash command such asls. Claude Code should block the call with this error:
PreToolUse:Bash hook error: [node ${CLAUDE_PROJECT_DIR}/.claude/hooks/check-command.js]: failed; blocking because onFailure is "block"
Without onFailure, the same missing script is a non-blocking error and ls runs. A timeout says timed out in place of failed.
Then test the case onFailure cannot catch. Run the script with jq missing or given input that is not JSON, and confirm it exits 2.
- Open each script and make its own failures exit 2, as in the
trapabove. - Move whatever a pattern can state into deny rules. The settings reference gives
Read(./.env)as an example rule, which matches reads of the.envfile. - If you leave a guard open on purpose, make its failure visible. The Developers Digest tester suggests a
doctorcommand, a log line, or aSessionStartcheck, which is a hook that runs when a session begins, that the guard script exists and runs.
The same fail-open default shows up outside hooks.
If the sandbox cannot start, Claude Code "runs commands without sandboxing" unless you set sandbox.failIfUnavailable to true. Cursor's hooks fail open by default on crashes, timeouts and non-zero exit codes other than 2, and failClosed: true flips that.
For where hooks sit beside skills, plugins and MCP servers, Build to Launch's Claude Code hub maps the pieces, and its comparison of MCP, plugins and skills covers the choice among them.
FAQ
Do Claude Code hooks fail open?
Yes, by default. On most events, a hook can fail to start, time out or exit with a code other than 2 without stopping the action. The current docs name "continue" as the default for onFailure.
What does onFailure "block" do in Claude Code?
It makes a failed command or HTTP hook block the action. The v2.1.295 release notes list three failures: a failed start, a timeout, and an unexpected exit code.
Which Claude Code version added onFailure?
Version 2.1.295, released on October 8, 2026, according to its release notes. The reference now documents the field.
Does onFailure "block" catch a guard exiting 0 by mistake?
No. One tester's guard exited 0 on a machine without jq, and onFailure: "block" would not have stopped it. Write the script so its own errors exit 2.
Sources
- Claude Code v2.1.295 release notes, Anthropic, October 8, 2026
- Claude Code v2.1.294 release notes, Anthropic, October 8, 2026
- Hooks reference, Claude Code Docs, read October 14, 2026
- Configure permissions, Claude Code Docs
- Configure the sandboxed Bash tool, Claude Code Docs
- Hooks, Cursor Docs
- Configure settings, Claude Code Docs, read October 14, 2026
- Issue 100891: onFailure "block" does not block when a Windows hook exits 0xFFFD0000, anthropics/claude-code, opened October 9, 2026
- Secret scanning for AI coding tools, GitGuardian documentation
- Do Your Agent Hooks Fail Open? Claude Code vs Codex vs Cursor vs Gemini CLI, Developers Digest, October 9, 2026
- Major Updates in Claude Code v2.1.294~v2.1.295, DevelopersIO (Classmethod), October 9, 2026
- Claude Code 2.1.295 lets a failed hook block, and its docs still say not to rely on one, MIXED, October 8, 2026
- Claude Code hooks as guardrails, Himanshu Shukla
- Everything Claude Code: The Complete Guide Hub, Build to Launch
- Claude Code MCP vs Skills vs Plugins: Complete Decision Guide, Build to Launch