Claude Code 钩子出错时会放行:989 个公开守卫仓库里只有 18 个设了 onFailure
PreToolUse 钩子崩溃、超时或 exit 1 时,工具调用照样执行。989 个有拦截型 PreToolUse 钩子的公开仓库里,18 个设了 onFailure。我们实测了三种情况,并数了仓库的写法。
概述
如果你拿 Claude Code 的 PreToolUse 钩子当守卫,给它加上 "onFailure": "block",并且确认脚本拦截时用的是 exit 2,而不是 exit 1。
原因很简单。守卫钩子只有在跑起来、并且说了不行的时候才会拦。它一出错,工具调用就照样执行。文档把下面这几种都算作失败,失败就放行:
- 启动不了。 脚本路径写错了,或者机器上没有
node、python。 - 退出码不是 0 也不是 2。 包括 Unix 里最常见的错误码
exit 1。 - 超时。 command 钩子默认等 600 秒。
- 输出的 JSON 不合法。
从 2.1.295(10 月 8 日)开始,有一个字段能改掉这个行为:把 onFailure 设成 "block",上面任何一种失败都会改成拦下这次调用。
公开仓库里的守卫用上了吗?我们读了 5,864 个活跃公开仓库的项目设置。其中 989 个有能拦截调用的 PreToolUse 钩子,设了 onFailure 的只有 18 个,大约 55 个里有 1 个。
怎么测的
-
行为实测。 在一个空项目里给 Bash 配一个
PreToolUse钩子,让 Claude Code 2.1.314(claude -p,Haiku)执行echo ran > ran.txt。三种情况:- 钩子打印
BLOCK: not allowed后 exit 1:命令执行了。 - 钩子要跑的脚本不存在,没设
onFailure:命令执行了。 - 同样不存在的脚本,加上
"onFailure": "block":命令被拦下,报错里写着failed; blocking because onFailure is "block"。
第一个钩子还把自己拿到的环境变量名和 stdin 存了下来。工具参数只在 stdin 里,是一段 JSON。环境变量里没有
CLAUDE_FILE_PATH,也没有CLAUDE_TOOL_INPUT。 - 钩子打印
-
仓库样本。 和我们的项目设置调查同一批:5,864 个公开仓库,5 星以上,10 月 7 日以后有推送,排除 fork,截至 2026 年 11 月 6 日。我们读了其中每个
.claude/settings.json,以及钩子点名的脚本。 -
哪些钩子会拦截。 逐个读
PreToolUse命令钩子和它调用的脚本,不运行任何东西。能 exit 2,或者能输出deny、block决定的,算拦截型。转去调用我们读不到的代码的,算判不清,单独列出。
测试结果
有多少守卫出错时会拦
| 仓库数 | |
|---|---|
有 PreToolUse 命令钩子 | 1,673(占 5,864 的 28.5%) |
| 其中有能拦截的钩子 | 989(59.1%) |
再其中,在 PreToolUse 钩子上设了 onFailure | 18(1.8%) |
给 PreToolUse 钩子设了 timeout | 675(占 1,673 的 40.3%) |
把 376 个判不清的仓库全算成拦截型,设了 onFailure 的比例也不到 2%。所有仓库的所有钩子里,onFailure 一共出现 87 次:85 次是 "block",2 次是 "continue"。
根本拦不住的守卫
先猜一下:一个守卫打印了 BLOCK,然后 exit 1,会发生什么?调用照样执行。我们看到两种常见写法,最后什么都没拦住:
| 钩子数 | 仓库数 | |
|---|---|---|
打印 Blocked:、not allowed 这类拦截提示后 exit 1,全文没有 exit 2 也没有 deny | 53 | 42 |
读 $CLAUDE_FILE_PATH、$CLAUDE_TOOL_INPUT 这类 Claude Code 根本不设的变量 | 61 | 41 |
第一类里有一种很典型的一行写法:检查文件路径里有没有 .env,打印一句这次编辑被拦截,然后 exit 1。Claude Code 只会在记录里显示一条钩子报错,编辑照样做完。
第二类更隐蔽。变量是空的,所以 grep -q 'rm -rf' 这样的匹配永远不会命中,钩子每次都 exit 0。这 61 个里有 25 个,真命中的话本来会 exit 2 拦下来。
只加这个字段会改变行为的守卫
110 个拦截型钩子(59 个仓库)在别的地方也会 exit 1,常见的是发警告,或者缺少某个工具。加上 onFailure: "block" 以后,这些 exit 1 也都会拦下调用。文档给的改法是:发警告的地方改成 exit 0,警告内容放进输出的 systemMessage。
推荐
我们现在对每个守卫钩子都这样做,按顺序:
- 拦截用 exit 2,或者 exit 0 加 JSON 的 deny。 exit 1 只是一条警告,调用不会理它。
- 从 stdin 读工具参数。 比如 Bash 用
jq -r '.tool_input.command',Edit 和 Write 用.tool_input.file_path。 - 加上
"onFailure": "block"。 这样脚本丢了、崩了、超时了,都会拦下调用,而不是放过去。它对command和http钩子有效。 - 设一个短的
timeout。 设了onFailure以后超时也会拦,所以几秒比默认的十分钟更稳。 - 故意弄坏一次试试。 把脚本改个名,让 Claude 跑一次
ls,应该能看到blocking because onFailure is "block"。
你的守卫里,哪一个在今天脚本丢失时会放过 rm -rf?
该忽略什么
到处加 onFailure。它对 Stop、SubagentStop、TaskCompleted、TeammateIdle 钩子和用 async 跑的钩子不起作用。格式化、记日志这类钩子,出错时把编辑拦下来只会碍事。只给真正守着东西的钩子加。
blockWritesOutsideWorkingDirectories 那个 0。这个权限设置是 2.1.314 在 10 月 30 日加的,刚上线一周,现在的数说明不了什么。
局限
- 只做了静态读取。 我们只跑了三个测试钩子,没有跑仓库里的钩子。靠我们读不到的代码去拦截的脚本,都归在判不清那一组。
- 按关键词判断。 exit 1 旁边写着拦截提示,很像守卫,但这 53 个里可能有几个本意就是警告。
- 是样本。 5 星以上的活跃仓库,截至 2026 年 11 月 6 日,按代码搜索的相关度选出来,不是随机样本。
- 只测了一个版本。 我们测的是 Claude Code 2.1.314。在没有 Git Bash 的 Windows 上,shell 形式的
PreToolUse钩子出错时,就算没设onFailure也可能拦下调用,所以这里说的是 macOS 和 Linux 上的行为。
自己复现
计数在 hooks-onfailure-2026-11.json 里,不含仓库名和钩子内容。想测自己的守卫:加上 "onFailure": "block",把它调用的脚本改个名,再让 Claude Code 跑一次这个钩子匹配的工具。
设置文件的其他部分,见5,864 个公开仓库在 .claude/settings.json 里打开了什么。我们所有的检查项都在一页上:Claude Code 安全检查清单。