Claude Code 安全钩子异常也可能放行命令
文章指出,Claude Code 的部分钩子超时或异常退出时,工具调用仍可能继续,削弱依赖钩子的命令拦截。文中介绍2.1.295新增的 onFailure: "block" 配置,用于在钩子执行失败时阻断调用。
AI 深度解读
给 Claude Code 配了拦截危险命令的 hook,就能放心放开 Bash 权限吗?原文指出一个容易漏掉的细节:hook 超时或异常退出时,命令默认仍会进入正常权限流程;如果已有 allow 规则,它就可能直接执行。
作者用本地 Git 仓库复现了这种情况,再用 Claude Code 2.1.295 新增的 onFailure: "block" 改变故障行为。这篇最值得看的,是怎样验证一道防线在自身出错时还能不能守住。
拦截器出错,为什么危险命令反而能执行
原文讨论的是执行工具前运行的 PreToolUse hook。它接收工具调用信息,可以拒绝操作、要求确认,也可以让调用继续。
问题在于,正常拒绝和程序故障走的是不同路径。默认情况下,command、http 或 mcp_tool hook 超时不会阻止工具调用;命令 hook 以非 0、非 2 的退出码结束,也被视为非阻断错误。
hook 没能完成检查,不等于检查结果是拒绝。
假设团队允许执行 Bash,再靠 hook 拦住少数危险命令。只要 hook 因输入结构变化抛异常,或等待某个依赖超时,调用就会回到正常权限流程。已有 allow 规则匹配时,命令便能继续执行,故障可能只留下会话记录。
作者为了排除人工确认的干扰,专门允许了 Bash(git push *)。健康的 hook 仍能覆盖这条 allow 规则;hook 故障后,allow 才成为命令通过的通道。
这里有个前提:没有匹配的 allow 规则时,故障后的调用仍可能遇到权限确认。因此,不能把原文简化成「hook 一超时,任何命令都会执行」。

用两次故障,检查规则是否真的守得住
作者把实验放在可丢弃的仓库里,origin 指向本机相邻目录下的 bare Git 仓库,避免测试推送影响远程项目。初始化涉及 git init -q -b main、git init -q --bare ../hook-lab-remote.git 和 git remote add origin ../hook-lab-remote.git。
每次尝试前,用 git commit -q --allow-empty -m "try" 创建新提交;尝试后运行 git ls-remote origin main,与 git rev-parse HEAD 比较。哈希相同才说明推送成功,不能只看 Claude 的文字回复。
下面这段 .claude/hooks/guard.py 负责拒绝向 main、master 推送,并在创建公开仓库前要求确认:
#!/usr/bin/env python3
"""PreToolUse guard for Bash calls."""
import json
import re
import sys
PUSH_MAIN = re.compile(r"\bgit\s+push\b.*\b(main|master)\b")
PUBLIC_REPO = re.compile(r"\bgh\s+repo\s+create\b.*--public\b")
def decide(decision, reason):
out = {"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": decision,
"permissionDecisionReason": reason,
}}
print(json.dumps(out))
sys.exit(0)
data = json.load(sys.stdin)
command = data["tool_input"]["command"]
if PUSH_MAIN.search(command):
decide("deny", "Pushing to main is blocked. Push a branch.")
if PUBLIC_REPO.search(command):
decide("ask", "This creates a PUBLIC GitHub repository.")
sys.exit(0)
要看的是 decide() 最后的 sys.exit(0):允许、拒绝、询问由 JSON 表达,退出码 0 表示 hook 正常完成。没有命中规则时,脚本无输出并退出 0,调用继续走正常权限流程。
原文用下面的 .claude/settings.json 接入脚本:
{
"permissions": {
"allow": ["Bash(git push *)"]
},
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3",
"args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/guard.py"],
"timeout": 5
}
]
}
]
}
}
这里的 timeout 单位是秒;命令 hook 默认值为 600,也就是十分钟。args 则让 python3 直接接收脚本路径,不经过 shell,符合原文引用的路径占位符使用建议。
先用管道给脚本传入 {"tool_input":{"command":"git push origin main"}},确认得到 deny JSON 和退出码 0。再启动 claude,通过 /hooks 检查加载情况,然后让它执行推送。
接下来故意破坏脚本:
- 在
import sys后添加import time; time.sleep(15),超过配置的5秒超时。 - 删除休眠,改为向 stderr 输出
guard broke,随后执行sys.exit(3)。
作者报告,两种情况下推送都成功了。普通 Python 异常通常退出 1,在没有有效 JSON 输出时,同样可能进入非阻断错误路径。
这套实验的价值是:它检查了脚本正常、挂起、崩溃三种状态,并用仓库实际状态验证结果。
加上故障阻断,还要收窄它的影响范围
按原文,onFailure 需要 Claude Code 2.1.295 或更高版本;该版本发布于 2026 年 10 月 8 日,可以先用 claude --version 检查。
在 handler 的 "timeout": 5 后加上 "onFailure": "block",保留刚才的故障再次测试。作者报告,退出 3 和超时都会阻止推送,远端哈希保持不变。
原文列出的故障还包括 hook 无法启动、HTTP 错误,以及 JSON 无法解析或校验失败。单纯输出普通文本不属于这组 JSON 故障。开启后,即使先输出 permissionDecision: "allow",再退出 1,也会被视为失败并阻断。
代价也直接:matcher 为 Bash 时,脚本里的一个拼写错误,可能连 ls 都拦住。
作者建议为不同规则配置独立 handler,通过 if 收窄范围,例如 "if": "Bash(git push *)",另一个使用 "if": "Bash(gh repo create *)"。每个 if 只容纳一条权限规则;当 Claude Code 无法判断 Bash 输入包含什么命令时,仍会运行 hook。原文也明确说,这个过滤属于尽力匹配。
另一些限制不能遗漏:onFailure 对 Stop、SubagentStop、TaskCompleted、TeammateIdle,以及设置了 async 或 asyncRewake 的 hook 不生效。用于阻断的检查不能随手改成异步。原文还提醒,处理 tool.check 的 mod 可能批准已被 PreToolUse 拒绝的调用,除非该 hook 位于 managed settings。
Windows 用户另需留意 2026 年 10 月 14 日发布的 2.1.299:原文称,它修复了未安装 Git for Windows、hook 又未指定 shell 时,PreToolUse 故障放行的问题。
我的判断
作者指出的问题成立:如果一项检查承担了阻止高风险操作的职责,它故障时继续放行,会削弱整个权限设计。onFailure: "block" 把故障后的行为明确写进配置,比依赖日志里有人发现报错更可靠。
但它只解决「检查没跑完怎么办」,没有解决「检查规则是否完整」。
示例正则会漏掉当前分支为 main 时的裸 git push,也无法完整处理 git -C、shell 别名等写法;另一方面,fix-main-nav 这样的分支名又可能被误拦。开启故障阻断,不会让字符串匹配自动理解 Git 语义。
hook 适合补充操作提醒,主分支保护仍应由仓库服务端承担。
与 hook 相比,权限系统适合表达硬性的允许、拒绝规则;带推送限制的 branch protection,则不依赖开发者本机脚本是否存在、是否正常运行。三者解决的问题各有范围。
原文的两次故障属于作者报告的实测,但没有提供一般团队的误拦率、维护成本或效率对照。5 秒对本例的两个正则足够,是具体场景下的判断,不能直接推广到需要网络请求的检查。
Hermes 部分还带有作者推广自身产品的立场。文中提到 Hermes 2.3.4 于 2026 年 10 月 15 日发布,使用 BSL 1.1,支持 macOS、Windows、Linux;这些并不是验证故障阻断所必需的条件。真正有用的提醒是:新 worktree 会带上已跟踪文件,因此团队规则应提交 .claude/settings.json 和脚本,不能只放在 .claude/settings.local.json。
能拿来做什么
- 检查现有 Claude Code 配置里,哪些操作已经通过 allow 放行、实际依赖 hook 兜底。对承担阻断职责的同步检查,核对版本并明确配置
onFailure: "block"。 - 在本地 bare 仓库复现正常拒绝、超时和异常退出,使用
git ls-remote origin main核验结果,测试结束后删除故障代码。 - 按操作范围拆分 handler,避免一个检查损坏后阻断所有 Bash 调用;同时把主分支限制落实到服务端 branch protection。
- 在使用 worktree 的 AI 编程流程中,确认规则文件会随任务检出。测试
gh repo create --public时拒绝确认,因为已登录的gh可能真的创建公开仓库。