Blackbox Blast
BB Preflight
← Incident log

CASE · Major · monitoring-gap

Unverified reportCollected from public reports

A secret-scan hook logged 'blocked' and the push went through 10 seconds later: exit 1 did not block (unverified report)

According to the author, a PreToolUse hook ran a secret-scanning check before Claude Code's git push. One July morning the check's log held two lines for the same commit: at 10:04:58 'stopped on suspected secret', and 10 seconds later 'pushed'. The commit had actually reached the remote. The repository was private.

5 min read

OBSERVED
CATEGORY
monitoring-gap
BLAST RADIUS
none
DETECTED BY
log-review
TAGS
#claude-code #hooks #pretooluse #exit-code #git-push #secret-scan #silent-failure #onfailure

CH 1

Cause

The author believes the cause was the exit code. The check script called sys.exit(1) to block, but per the author only exit code 2 blocks an action in a Claude Code PreToolUse hook; any other non-zero value, including 1, is treated as a non-blocking error. The author had assumed it worked like git's own pre-push hook. The tests only checked that 1 was returned when something was found, not that the action really stopped, so they all passed.

CH 2

Consequence

The author rebuilt the same setup on Claude Code 2.1.316 in November and ran 11 variants three times each, 33 runs in total. Without onFailure, the action ran all three times for exit 1, exit 3, a check that crashed on an exception, and a misspelled filename passed to bash. Only exit 2 and a misspelled filename passed to python (python exited with 2) stopped it. In the 12 runs where the action ran although it should have been blocked, none of the claude -p replies mentioned the hook. The author states that a hook of this shape had been blocking nothing.

CH 3

Fix

The author changed every blocking path to exit code 2 the same day. The author also reports that adding "onFailure": "block" (available from 2.1.295, documented on the official page) stopped the action in all four failure forms. It also blocks hooks that return exit 1 only to warn, so such hooks must be found and changed to exit 0 first. The author recommends confirming any blocking check once with a deliberately triggering value.

What happened

In the author's environment, a PreToolUse hook ran a check script before Claude Code's git push, looking for secrets such as API keys. When the check found something, the script was built to exit with code 1. One July morning the author looked at the check's log and found two lines for the same commit: at 10:04:58 'stopped on suspected secret', and 10 seconds later 'pushed'. Checking the remote showed that the commit had arrived. The repository was private. This is an individual's report and does not describe other harm.

The chaos on the ground

Per the author, that morning was the first time the check had issued a blocking verdict in real use; until then every push from the main repository had passed. So it was not clear until that morning that the blocking verdict stopped nothing. The tests checked only that 1 was returned when something was found, not that the action stopped, so they all passed.

In November the author ran an experiment to confirm the cause: Windows 11, Claude Code 2.1.316, Python 3.12.10, with hooks called from Git Bash. Besides exit code values, the author added a check crashing on an exception and a misspelled filename in the settings, made 11 variants, and ran each three times with claude -p, 33 runs in all. Without onFailure the results were:

  • exit 0, 1, 3, a crash on an exception, and a misspelled filename passed to bash: the action ran in all three runs
  • exit 2 and a misspelled filename passed to python: stopped in all three runs

In the 12 runs where the action ran although it should have been blocked, none of the claude -p replies mentioned the hook. The author says the official page states that the interactive screen shows a one-line hook error in its log. The author tested only claude -p, not the interactive screen. The author notes that reading only the claude -p reply, one cannot tell that the action was not stopped.

Root cause

The author believes the cause was the exit code. The check script called sys.exit(1) to block, but per the author only exit code 2 blocks in a PreToolUse hook, and every other non-zero value, 1 included, is treated as an error that does not block. The author had assumed otherwise because 1 is the conventional failure value and git's own pre-push hook blocks on any non-zero code. The Claude Code version in use in July was not recorded, so what happened then cannot be confirmed, the author says.

The experiment also exposed a form that is easy to overlook. Python returns exit code 1 when it dies on an uncaught exception, so if the check fails to read its settings file and crashes, the action goes through with nothing checked. With a misspelled filename, python was handed a file it could not open and exited with 2, which happened to count as a block; bash returned 127 for the same typo and the action went through. Whether a configuration typo stops the action thus depended on whether the command was run by python or bash.

The fix

The author changed every place that blocks, in the check script and the settings loader, to exit code 2 on the day of the incident. When a hook stops with 2, the reason written to standard error is passed to Claude.

The other measure is adding "onFailure": "block" to the hook configuration. Per the author, it is an item on the official hooks page, available from 2.1.295. According to that page, it counts exit codes other than 0 and 2, startup failures and timeouts as blocking. The author did not test the timeout case. In the four variants with onFailure added (exit 1, an exception crash, and misspelled filenames for python and bash), the action did not run in any of them. The author warns that it also blocks hooks that return exit 1 only to warn, so before adding it one should look for such hooks and change them to exit 0.

As a lesson, the author recommends that once a blocking check is in place, it be tested at least once with a value that deliberately triggers it, to see that the action does not run. Until then it cannot be called a check that blocks.

Sources

PREFLIGHTIs your own Mac prepared?BB Preflight is a free Mac tool that checks 21 things, from AI agent permissions to where your keys live, without changing anything.Run the check →