Blog · 2026-11-06

Claude Code hooks let the call through when they fail: 18 of 989 public guard repos set onFailure

A PreToolUse hook that crashes, times out or exits 1 lets the tool call run. Of 989 public repos with a blocking PreToolUse hook, 18 set onFailure. We tested the three cases and counted what repos do.

Summary

If you use a Claude Code PreToolUse hook as a guard, add "onFailure": "block" to it, and make sure the script blocks with exit 2, not exit 1.

The reason is simple. A guard hook only blocks when it runs and says no. When it fails, the tool call goes ahead. The docs count all of these as failures that let the call through:

  • It can’t start. The script path is wrong, or node or python is missing.
  • It exits with a code other than 0 or 2. That includes exit 1, the usual Unix error code.
  • It times out. The default is 600 seconds for a command hook.
  • It prints invalid JSON.

Since version 2.1.295 (October 8), one field changes that: with onFailure set to "block", any of these failures blocks the call instead.

How many public guards use it? We read the project settings of 5,864 active public repos. 989 have a PreToolUse hook that can block a call. 18 of them set onFailure, about 1 in 55.

How we tested

  1. The behaviour. In an empty project, we set up a PreToolUse hook on Bash and asked Claude Code 2.1.314 (claude -p, Haiku) to run echo ran > ran.txt. Three cases:

    • A hook that prints BLOCK: not allowed and exits 1: the command ran.
    • A hook whose script doesn’t exist, with no onFailure: the command ran.
    • The same missing script with "onFailure": "block": the command was blocked, with the error failed; blocking because onFailure is "block".

    The first hook also saved its environment variable names and its stdin. The tool input was only on stdin, as JSON. No variable named CLAUDE_FILE_PATH or CLAUDE_TOOL_INPUT was set.

  2. The repos. The same sample as our project settings review: 5,864 public repos with at least 5 stars, pushed to since October 7, not forks, as of November 6, 2026. We read every .claude/settings.json in them and the scripts their hooks name.

  3. Which hooks can block. We read each PreToolUse command hook and the script it calls, without running anything. A hook counts as blocking if it can exit 2 or print a deny or block decision. A hook that hands off to code we couldn’t read counts as unclear, and we keep those apart.

Results

How many guards fail closed

Repos
Have a PreToolUse command hook1,673 (28.5% of 5,864)
Of those, have a hook that can block989 (59.1%)
Of those, set onFailure on a PreToolUse hook18 (1.8%)
Set a timeout on a PreToolUse hook675 (40.3% of 1,673)

Even counting the 376 unclear repos as blocking, the share that sets onFailure stays under 2%. Across all hooks in all repos, onFailure appears 87 times: 85 set to "block" and 2 set to "continue".

Guards that never block

Quick guess: what does a guard do when it prints BLOCK and exits 1? It lets the call through. We found two common ways a guard ends up blocking nothing:

HooksRepos
Print a block message such as Blocked: or not allowed, then exit 1, with no exit 2 or deny anywhere5342
Read $CLAUDE_FILE_PATH, $CLAUDE_TOOL_INPUT or a similar variable that Claude Code doesn’t set6141

A typical one-liner from the first group checks whether the file path contains .env, prints that the edit is blocked and exits 1. Claude Code shows a hook error notice and makes the edit.

The second group is quieter. The variable is empty, so a pattern such as grep -q 'rm -rf' never matches, and the hook exits 0 every time. 25 of those 61 hooks would block with exit 2 if they ever matched.

Guards that would change if you only added the field

110 blocking hooks in 59 repos also exit 1 somewhere, often for a warning or a missing tool. With onFailure: "block", every one of those exits blocks the call too. The docs give the fix: exit 0 where you warn, and print the warning as a systemMessage.

Our picks

What we now do with every guard hook, in order:

  1. Block with exit 2, or with a JSON deny on exit 0. Exit 1 is a warning that the call ignores.
  2. Read the tool input from stdin. For example, jq -r '.tool_input.command' for Bash or .tool_input.file_path for Edit and Write.
  3. Add "onFailure": "block". Then a missing script, a crash or a timeout blocks the call instead of passing it. It works on command and http hooks.
  4. Set a short timeout. With onFailure set, a timeout blocks, so a few seconds is safer than ten minutes of waiting.
  5. Test the failure. Rename the script and ask Claude to run ls. You should see blocking because onFailure is "block".

Which of your guards would let rm -rf through if its script went missing today?

What to ignore

Adding onFailure everywhere. It has no effect on Stop, SubagentStop, TaskCompleted and TeammateIdle hooks, or on hooks that run with async. On a formatter or a logger, a failure that stops the edit only gets in your way. Put it on the hooks that guard something.

The 0 we found for blockWritesOutsideWorkingDirectories, the permissions setting added in 2.1.314 on October 30. It is a week old, so the count says nothing yet.

Limitations

  • Static reading. We ran three test hooks, not the repos’ hooks. A script that blocks through code we couldn’t read is in the unclear group.
  • Keyword matching. Exit 1 next to a block message is a strong sign of a guard, but some of those 53 hooks may mean to warn.
  • A sample. Active repos with 5 or more stars, as of November 6, 2026, chosen by code search relevance. It is not a random sample.
  • One version. We tested on Claude Code 2.1.314. On Windows without Git Bash, a failing shell-form PreToolUse hook can block even with onFailure unset, so the numbers here describe macOS and Linux behaviour.

Reproduce it

The counts are in hooks-onfailure-2026-11.json, with repo names and hook contents left out. To test one of your own guards, add "onFailure": "block", rename the script it calls, and ask Claude Code to run the tool the hook matches.

For the rest of a repo’s settings file, see what 5,864 public repos turn on in .claude/settings.json. All of our checks are on one page: the Claude Code safety checklist.

Sources

  1. Claude Code docs: Hooks (Exit code output, Timeouts, Block the action when a hook fails) · official
  2. Claude Code CHANGELOG, 2.1.295: onFailure for command and HTTP hooks · official
  3. AgentCraft: Before you trust a cloned repo, read its .claude/settings.json · blog