Bible Network Crypto DeFi Onchain RWA AI Agent Stablecoin Chain SAFU CryptoTax DeFAI AGI Claude Me Claude Skill Claude Design Claude Cowork
Independent Media
Not affiliated with any project
Learn Claude Skills. Do Everything Better.
claudeskill-me.com
LATEST
Check Before Upgrading to Claude Code 2.1.299: Stricter MCP Allow Rules, Skill Names With Odd Characters Rejected, UserPromptSubmit Hooks Now Run First  ·  Claude Code Adds Idle Compaction: Conversations Over 200K Tokens Are Compacted After About 54 Idle Minutes — It Saves Cache Cost and May Drop Detail  ·  Fork Subagents Inherit the Whole Conversation Instead of Starting Blank — and Since v2.1.302 They Are On by Default in VS Code, Desktop and Headless Runs  ·  claude plugin eval Explained: Using the With/Without Delta (Δ) to Judge Whether a Skill Helps — and Why Δ Never Fails Your CI  ·  Claude Code Hooks Fail Open by Default: A Missing Script, a Timeout or Exit 1 Lets the Action Through — Using onFailure: "block" from v2.1.295  ·  Claude Haiku 5.5 Becomes Claude Code's Default Haiku: Input Drops to $0.10, but Crossing 100K Tokens Makes the Whole Request Five Times Pricier
practice

Claude Code Hooks Fail Open by Default: A Missing Script, a Timeout or Exit 1 Lets the Action Through — Using onFailure: "block" from v2.1.295

30-Second Version · For the impatient
A hook's exit 2 is the guard's verdict, but if the guard never shows up for work, the default outcome is to let the action through.

Full Explanation +
01 · Why did this happen?

I already have a PreToolUse hook that returns exit 2. Do I still need onFailure?

You should consider it. Exit 2 only takes effect when the script actually runs and deliberately returns 2. If the path is mistyped, the file lacks execute permission, it times out, or the script crashes midway and exits 1, the guard never "ruled" on anything and the default is to let the action through. onFailure: "Block" covers exactly the case where the guard did not show up for work, so the two are complementary.

02 · What is the mechanism?

How do I verify that my hook really fails closed?

Test in a throwaway directory with claude -p. Do three things separately: break the script path, add a sleep so it exceeds the timeout, and make the script exit 1 outright. For each, ask Claude to run a harmless command that should be blocked, confirm it gets blocked, then restore the configuration and confirm a safe command still passes. The Developers Digest test follows this idea and changes only the hook as a variable.

03 · How does it affect me?

Can prompt hooks and agent hooks use onFailure too?

Per the Developers Digest summary, not currently: onFailure applies only to command and HTTP hooks. For rules that need a hard stop, use a command hook as the guard and make it fail closed as described above; prompt and agent hooks are better suited to soft checks that need semantic judgment. v2.1.294 also notes that these two kinds, when written as instructions, previously let through actions they should have blocked, which is a sign they are a poor choice as the only line of defense.

04 · What should I do?

I'm not an engineer; I only look after the team's rules. How does this concern me?

If your team relies on hooks to enforce rules (for example no deleting files, no pushing straight to main), you can ask the engineers one simple question: "What happens when the guard script breaks or times out?" The default answer is that the action goes through, with only an easily missed error line on screen. Check whether anyone has set onFailure: "Block" for the critical rules, and whether someone periodically verifies that the gate still works.

Full Content +

Many people treat a hook as a security gate that "guarantees a Block," but the official hooks documentation is explicit: only exit code 2 blocks an action by itself. Any other non-zero exit does not block; it just shows a hook error line in the transcript. That means that when a guard script breaks, the default outcome is to let the action through. v2.1.295, released October 8, 2026, adds onFailure: "block" to close exactly this hole.

The default is fail-open: what lets an action through

According to the official hooks documentation, none of the following blocks a PreToolUse call:

  • A missing or non-executable script: an exit code like 127 is treated as a non-blocking error. The documentation itself warns that a mistyped path in settings.json leaves the gate silently disabled.
  • A timeout: when a command, http or mcp_tool hook times out, the tool call proceeds through the normal permission flow. The default timeout for these is 600 seconds, 30 seconds for prompt hooks and 60 seconds for agent hooks.
  • Exit code 1 or any other non-zero value: the usual Unix failure code is only a warning here; the documentation says to use exit 2 if the hook is meant to enforce a policy.
  • HTTP hook trouble: a non-2xx status, a connection failure, or a 2xx with a non-JSON body are all non-blocking errors. An HTTP hook cannot block on status code alone; it must return a 2xx JSON body with the decision fields.

onFailure: "block" in v2.1.295

The release notes describe it this way: command and HTTP hooks can be set to block the action, instead of letting it through, when the hook cannot start, times out, or exits with an unexpected code. A test published by Developers Digest on October 9 ran six configurations through claude -p on v2.1.295, changing only the PreToolUse hook: with nothing set, both a missing script and a timeout let the command run; with "onFailure": "block" both were blocked, while a working guard still allowed safe commands.

That write-up also states the scope: it matters only for events that can block, such as PreToolUse, UserPromptSubmit and Stop, and prompt, agent and MCP tool hooks are not covered. The configuration below follows that article's example (the page of the official hooks documentation I could read does not show this key, so confirm with the official docs or claude Plugin validate before relying on it):

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/guard.sh",
            "timeout": 5,
            "onFailure": "block"
          }
        ]
      }
    ]
  }
}

The guard script has to fail closed too

onFailure alone is not enough. If a script finishes but exits 0 because some command inside it failed, Claude Code sees a successful allow. The Developers Digest author found that a guard without error handling, on a machine without jq, exited 0, which would have let rm -rf through. The sturdier pattern is to make any unexpected error exit 2. Below is a sketch written on that principle (the matching rule is deliberately crude and only shows the flow):

#!/usr/bin/env bash
set -euo pipefail
trap 'echo "guard crashed" >&2; exit 2' ERR
cmd=$(jq -r '.tool_input.command')
if printf '%s' "$cmd" | grep -Eq 'rm +-rf +/'; then
  echo "blocked: destructive rm" >&2
  exit 2
fi

Here trap ... ERR turns a failure such as a missing jq into exit 2, while a grep inside an if condition that finds no match returns 1 without firing the trap.

Don't set everything to block

Failing closed has a cost: if the HTTP endpoint your guard depends on goes down, every Bash call is blocked, and a convenience hook such as a formatter or notifier does not need to stall a whole session when it breaks. The following is my recommendation, an inference rather than official policy:

  • Set onFailure: "block" only for irreversible or abusable actions (deletes, pushes, credential exfiltration).
  • Pair it with a short timeout. The official default is 600 seconds, so a hung guard can make an action wait up to ten minutes.
  • A regex guard is one layer; combine it with a Sandbox or deny rules.

Related fixes in the same weeks

v2.1.294 (October 8) fixed prompt and agent hooks written as instructions (for example "Block commands that…") letting through what they should block, and improved how prompt hooks on Stop and SubagentStop are judged so Claude is less likely to stop early. A summary of the earlier v2.1.288 notes also mentions that a PreToolUse hook skipped because of a matching failure now blocks the call (see the original notes for details). With several "the gate didn't hold" fixes landing within one month, it is worth re-testing your own hooks.

Sources: Hooks reference - Claude Code Docs, Do Your Agent Hooks Fail Open? Claude Code vs Codex vs Cursor vs Gemini CLI - Developers Digest, Claude Code changelog - Claude Code Docs, Claude Code 2.1.295 - hooks can fail closed - ai-tldr.dev
Diagram
hook 失敗時的預設放行與 onFailure: block 的差別腳本不存在、逾時、非預期退出碼三種失敗,預設都會放行動作;在 command 或 HTTP hook 設定 onFailure: block 後改為阻擋Hook Failure Paths: Default Open vs onFailure: blockScript missingexit 127, bad pathTimeoutdefault 600s (command/http)Unexpected exitexit 1 or other non-zeroIs onFailure set to"block" on this hook?command / HTTP hooks onlyNo (default): fail-openAction proceedsgate silently offYes: fail-closedAction is blockedv2.1.295 and laterAlways: exit 2 blocks on blockable events, exit 0 allowsprompt / agent / MCP tool hooks are not covered by onFailureGuard script itself: set -euo pipefail + ERR trap that exits 2Claude Skill Me · claudeskill-me.com
Feel free to share. Please credit the source.
Ask a Question
Please enter at least 10 characters
Related Articles
Your Hook Says "Blocking Error" But the File Still Changed? PostToolUse and PreToolUse Don't Actually Block the Same Way
practice · Sep 28
Check Before Upgrading to Claude Code 2.1.299: Stricter MCP Allow Rules, Skill Names With Odd Characters Rejected, UserPromptSubmit Hooks Now Run First
practice · Oct 19
CLAUDE.md Said So, but Claude Still Skipped It? Use Hooks to Turn a Request into a Guarantee
practice · Aug 25
Fork Subagents Inherit the Whole Conversation Instead of Starting Blank — and Since v2.1.302 They Are On by Default in VS Code, Desktop and Headless Runs
advanced · Oct 19
Related News
More Related Topics