📚AI Agents & Security

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.

By VibeCoding.Builders
7 minute read
0 views
0 bookmarks
Tags
claude-codehooksonfailureguardrailsagent-safetyanthropic

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.

How a Claude Code guard hook can endA Claude Code hook has three outcomes: exit 0 lets the action go ahead, exit 2 blocks it, and any other failure follows the onFailure setting, which continues by default.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 breaksDefault, continueWith onFailure: "block"
The script path is wrongRuns anywayBlocked
The script hangs past its timeoutCommand runsBlocked
The script exits 1The prompt goes throughBlocked
The script exits 0 by mistakeThe command goes aheadThe command goes ahead
A Windows powershell.exe -File script is missingRuns anywayThe 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?

  1. Find every hook that can block. Look for hooks on events where exit 2 blocks, such as PreToolUse and UserPromptSubmit, in .claude/settings.json and ~/.claude/settings.json. The section "Exit code 2 behavior per event" in Anthropic's reference lists which events can. Then run claude --version to confirm v2.1.295 or later.
  2. Add onFailure to each guard that stands between a model and something you cannot undo. This is Anthropic's example, a PreToolUse hook on Bash:
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "node",
            "args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/check-command.js"],
            "onFailure": "block"
          }
        ]
      }
    ]
  }
}
  1. Add a short timeout, in seconds, to the same hook. The Developers Digest example uses 5.
  2. Break each guard on purpose. Anthropic's own test is to leave check-command.js missing and ask Claude to run a Bash command such as ls. 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.

  1. Open each script and make its own failures exit 2, as in the trap above.
  2. Move whatever a pattern can state into deny rules. The settings reference gives Read(./.env) as an example rule, which matches reads of the .env file.
  3. If you leave a guard open on purpose, make its failure visible. The Developers Digest tester suggests a doctor command, a log line, or a SessionStart check, 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

External Resources

📖

Claude Code v2.1.295 release notes

documentation•https://github.com/anthropics/claude-code/releases/tag/v2.1.295
📖

Claude Code v2.1.294 release notes

documentation•https://github.com/anthropics/claude-code/releases/tag/v2.1.294
📖

Hooks reference, Claude Code Docs

documentation•https://code.claude.com/docs/en/hooks
📖

Configure permissions, Claude Code Docs

documentation•https://code.claude.com/docs/en/permissions
📖

Configure the sandboxed Bash tool, Claude Code Docs

documentation•https://code.claude.com/docs/en/sandboxing
📖

Hooks, Cursor Docs

documentation•https://cursor.com/docs/hooks
📖

Issue 100891: onFailure "block" does not block when a Windows hook exits 0xFFFD0000

documentation•https://github.com/anthropics/claude-code/issues/100891
📖

Secret scanning for AI coding tools, GitGuardian documentation

documentation•https://docs.gitguardian.com/ggshield-docs/integrations/ai-coding-tools/secret-scanning-for-ai-coding-tools
📄

Do Your Agent Hooks Fail Open? Claude Code vs Codex vs Cursor vs Gemini CLI, Developers Digest

article•https://www.developersdigest.tech/blog/coding-agent-hooks-fail-open-vs-fail-closed-2026
📄

Major Updates in Claude Code v2.1.294~v2.1.295, DevelopersIO (Classmethod)

article•https://dev.classmethod.jp/en/articles/20261009-cc-updates-v2-1-295/
📄

Claude Code 2.1.295 lets a failed hook block, and its docs still say not to rely on one, MIXED

article•https://mixed-news.com/en/claude-code-2-1-295-onfailure-block-hooks-docs-contradict
📄

Claude Code hooks as guardrails, Himanshu Shukla

article•https://thehimanshushukla.com/blog/claude-code-hooks-as-guardrails
📄

Everything Claude Code: The Complete Guide Hub, Build to Launch

article•https://buildtolaunch.substack.com/p/claude-code-hub
📄

Claude Code MCP vs Skills vs Plugins: Complete Decision Guide, Build to Launch

article•https://buildtolaunch.substack.com/p/claude-code-mcp-vs-plugins-vs-skills
📖

All settings, Claude Code Docs

documentation•https://code.claude.com/docs/en/settings-reference