你写了一个 PreToolUse hook 想拦敏感路径的 Edit,结果现在每次 Edit 都被拒——连改 README 这种无害操作都被拦。或者你在终端里手动跑这个脚本是 success,Claude Code 偏偏当成 block 处理。PreToolUse hook 是按 exit code 控制每次匹配的工具调用:0 放行、非 0 拒绝。Hook 通过 stdin 收到一个描述工具调用的 JSON,settings.json 里的 matcher pattern 决定它能看到哪些调用。多数「莫名 block」要么是 matcher 太宽、要么是 exit code 写错、要么是 stdin 解析失败装成了刻意拒绝。debug print 加一次手动测试,几分钟就能定位。
常见原因
按命中率从高到低。
1. Hook 脚本意外返回了非 0
常见模式:set -e 配合一个 fall-through 的 if、set -u 下用了未定义变量、或者最后一个命令在 happy path 也返回了 1。任何非 0 退出都会 block 工具调用。
怎么判断:手动给 hook 脚本喂一个示例 stdin payload 跑一次、然后 echo $?。success 分支看到不是 0 就是 bug。
2. Matcher pattern 比你以为的更宽
Matcher 写 Edit 会匹配所有 Edit 调用、不只是某个路径的。如果你的拒绝逻辑依赖路径检查、但脚本默认走 block,那就是全拒。
怎么判断:看 .claude/settings.json 或 ~/.claude/settings.json 里的 matcher 行。如果就是 "matcher": "Edit"、你本意是路径过滤,那 matcher 就太宽了。
3. Stdin JSON 解析静默失败
Hook 脚本通过 stdin 拿工具调用的 payload。如果你用 jq 解析、jq 报错了,你的脚本可能默认非 0 退出、看起来就像是刻意拒绝。
怎么判断:脚本开头加 jq . > /tmp/hook-input.json。触发一次工具调用、然后看这个文件。空的或非法的就是解析失败。
4. Hook 被覆盖或重复加载
如果项目 .claude/settings.json 和用户 ~/.claude/settings.json 都为 Edit 定义了 PreToolUse hook,两个都会跑。任何一个拒绝都会 block 这次调用。
怎么判断:在两个 settings 文件里搜同样 matcher 的 hook entry。先禁一个,看 block 是不是停了。
5. Stdout 的输出被当成了 deny 原因
某些 hook 版本会读 stdout 当 deny message。如果你不小心把 debug 信息打到了 stdout(不是 stderr),CLI 可能把它当成 block 原因显示出来。
怎么判断:看 Claude Code 打的 block message。里面如果包含你脚本里 log 的内容,就是 debug 输出漏到 stdout 了。
6. 全新 shell 下 hook 运行时缺失
如果 hook 用 python3 或 node、但启动 shell 的 PATH 里没有它,hook 还没读 stdin 就挂了。Claude Code 把这种挂当成 block。
怎么判断:在 Claude Code 用的同一个 shell 里跑 which python3。空的就是这个。
开始前
- 改 settings.json 之前先备份当前那份。
- 准备好临时禁用这个 hook 做对照。
- 用一次随手的 Edit(在 scratch 文件里改个注释)来测试。
- 装好
jq用来看 stdin payload。
需要收集的信息
- Claude Code 版本:
claude --version。 - Hook 脚本的完整内容和文件路径。
- settings.json 里的 matcher 条目。
- Block 发生时 Claude Code 打的 deny 信息。
- 一份 stdin payload 示例(用下面 Step 3 的招捕获)。
- Block 那一刻的
claude --debug输出。
一步一步修复
Step 1:隔离复现
对一个 scratch 文件做最小化 Edit。确认 block 触发。记下 deny 信息原文。
Step 2:临时禁用 hook
在 settings.json 里把这个 hook 条目注释掉、重启 Claude Code。同样的 Edit 现在能过,就确认是 hook 的问题(不是 permissions 或别的配置)。
Step 3:捕获 stdin payload
Hook 脚本头部加一行 debug:
#!/usr/bin/env bash
tee /tmp/hook-input.json | jq . > /dev/null
启用 hook、触发一次 Edit,看 /tmp/hook-input.json。里面应该有工具名、路径、改动内容。
Step 4:用捕获的 payload 手动测脚本
cat /tmp/hook-input.json | bash ~/.claude/hooks/my-hook.sh
echo "exit: $?"
应该过的 payload 手动跑出来 exit 非 0,就锁定逻辑 bug 了。
Step 5:收紧 matcher
只想拦 secrets/ 下的 Edit,就在 matcher 里或者脚本最前面做路径过滤:
{
"matcher": "Edit",
"hooks": [
{ "type": "command", "command": "~/.claude/hooks/block-secrets.sh" }
]
}
#!/usr/bin/env bash
path=$(jq -r '.tool_input.file_path' < /dev/stdin)
case "$path" in
*/secrets/*) echo "blocked: path under secrets/" >&2; exit 2 ;;
*) exit 0 ;;
esac
默认分支显式 exit 0 很关键。
Step 6:把 stdout 和 stderr 分开
Debug log 只往 stderr 打:
echo "checking $path" >&2
Stdout 留给官方的 deny-reason 通道(如果你的 CLI 版本用它)。
Step 7:固定 runtime
Hook 用了 python3 或 node 就用绝对路径:
#!/usr/bin/env bash
/usr/bin/env python3 ~/.claude/hooks/block.py
或者脚本头部 export 一个 PATH。
怎么验证修好了
- 在非敏感文件上的安全 Edit 不再被拦、也不弹 prompt。
- 故意对受保护路径下的文件做 Edit 会被拦、且 deny 信息符合预期。
- Deny 信息就是你脚本打的、不是 Claude Code 通用提示。
- 重启 Claude Code 后行为不回退。
长期预防
- Success 分支永远显式
exit 0,别靠隐式退出。 - 在 CI 里加一个小 harness,对每个 hook 脚本喂样例 payload 跑一遍。
- Matcher 尽量收窄,路径过滤能在 matcher 里做就在 matcher 里做。
- Debug 全部往 stderr 打、绝不往 stdout 打。
~/.claude/hooks/和 settings.json 纳入版本控制,回归时方便 bisect。
容易踩的坑
- 把
set -e当成显式 exit code 的替代品,它不是——一样可能漏个 1 出去。 - 写一个匹配
*的大 hook 然后里面 switch 工具名;小而专的 hook 更好调。 - 忘了 hook 跑在一个全新的非交互 shell 里、PATH 很瘦。
- print 打到 stdout 然后纳闷为什么 deny 信息变样了。
- 用 root 手动跑 hook 脚本、错过了 user context 才暴露的权限 bug。
常见问答
- 哪个 exit code 算放行? 0 放行,非 0 拦。
- 能看到 Claude Code 给 hook 发了啥吗? 能,stdin 用
tee落地一份就行。JSON payload 里有tool_name和tool_input。 - deny message 是哪里来的? 看 Claude Code 版本。有的读 stderr、有的读 stdout 上特定格式的 JSON 响应。看你那版的文档。
- 能按 diff 内容判断、而不只是路径吗? 能。Payload 里有 proposed change,用
jq解出来判断。 - Hook 对每个工具都跑、还是只跑 match 的? 只跑 match
matcher的。matcher 越具体越省事。 - 为什么 hook 在终端能跑、Claude Code 跑就挂? 几乎都是 PATH 或缺解释器。用绝对路径加干净的 shebang。
相关
- Claude Code settings.json 不生效
- Claude Code permissions 弹窗循环
- Claude Code Skill 没被发现
- Claude Code 改错文件
- Claude Code 半执行后卡住
标签: #Claude Code #hooks #排查 #排查