博客 · 2026-11-06

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 个。

怎么测的

  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。

  2. 仓库样本。 和我们的项目设置调查同一批:5,864 个公开仓库,5 星以上,10 月 7 日以后有推送,排除 fork,截至 2026 年 11 月 6 日。我们读了其中每个 .claude/settings.json,以及钩子点名的脚本。

  3. 哪些钩子会拦截。 逐个读 PreToolUse 命令钩子和它调用的脚本,不运行任何东西。能 exit 2,或者能输出 deny、block 决定的,算拦截型。转去调用我们读不到的代码的,算判不清,单独列出。

测试结果

有多少守卫出错时会拦

仓库数
有 PreToolUse 命令钩子1,673(占 5,864 的 28.5%)
其中有能拦截的钩子989(59.1%)
再其中,在 PreToolUse 钩子上设了 onFailure18(1.8%)
给 PreToolUse 钩子设了 timeout675(占 1,673 的 40.3%)

把 376 个判不清的仓库全算成拦截型,设了 onFailure 的比例也不到 2%。所有仓库的所有钩子里,onFailure 一共出现 87 次:85 次是 "block",2 次是 "continue"。

根本拦不住的守卫

先猜一下:一个守卫打印了 BLOCK,然后 exit 1,会发生什么?调用照样执行。我们看到两种常见写法,最后什么都没拦住:

钩子数仓库数
打印 Blocked:、not allowed 这类拦截提示后 exit 1,全文没有 exit 2 也没有 deny5342
读 $CLAUDE_FILE_PATH、$CLAUDE_TOOL_INPUT 这类 Claude Code 根本不设的变量6141

第一类里有一种很典型的一行写法:检查文件路径里有没有 .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。

推荐

我们现在对每个守卫钩子都这样做,按顺序:

  1. 拦截用 exit 2,或者 exit 0 加 JSON 的 deny。 exit 1 只是一条警告,调用不会理它。
  2. 从 stdin 读工具参数。 比如 Bash 用 jq -r '.tool_input.command',Edit 和 Write 用 .tool_input.file_path。
  3. 加上 "onFailure": "block"。 这样脚本丢了、崩了、超时了,都会拦下调用,而不是放过去。它对 command 和 http 钩子有效。
  4. 设一个短的 timeout。 设了 onFailure 以后超时也会拦,所以几秒比默认的十分钟更稳。
  5. 故意弄坏一次试试。 把脚本改个名,让 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 安全检查清单。

来源

  1. Claude Code 文档:Hooks(Exit code output、Timeouts、Block the action when a hook fails) · official
  2. Claude Code CHANGELOG,2.1.295:command 和 HTTP 钩子支持 onFailure · official
  3. AgentCraft:信任一个 clone 来的仓库之前,先读它的 .claude/settings.json · blog