DeepDojo.AI发布 2026-11-12English
Claude Code Hook 不触发怎么排查
你写了一个检查脚本,放进 .claude/hooks/,在终端里手动运行,输出一切正常。可接下来 Claude Code 编辑了十几个文件,这个脚本一次也没跑过。你改了脚本、加了日志,还是没有任何动静。
“Hook 没反应”是个很模糊的描述,背后可能是四层中的任意一层出了问题:
- 注册:脚本放在了
.claude/hooks/,但没有在 Settings 里登记; - 触发:选错了事件,或者 Matcher 没匹配上这次的工具;
- 输入:脚本读错了事件 JSON 的字段,遇到边界输入就出错;
- 结果:退出码和输出不符合约定,Claude Code 只报了个错就继续了。
开头这个例子卡在第一层:.claude/hooks/ 只是存脚本的地方,手动运行成功,不代表它已经注册。改脚本解决不了注册问题。所以排查 Hook 要先定位层级,再动手:用 /hooks 看注册,用日志看匹配,用最小 JSON 单独测脚本,最后核对退出码和两路输出。下面先把 Hook 的链路讲清楚,再按四层逐个查。
01 Hook 的链路:事件、Matcher、处理器、结果
Hook 在生命周期事件发生时运行处理器,适合编辑后格式化、执行前检查和等待确认时通知。触发无需依赖模型记住某句指令,但能否阻止操作,仍取决于事件时机和处理结果。
Hook 监听 Claude Code 运行中的事件,把事件 JSON 交给处理器。处理器就是实际做事的那部分:运行命令、调用 HTTP 服务,或请模型判断。链路是“事件 → Matcher → 处理器 → 结果”。

Claude Code 的 Hooks 参考文档对 Hook 的定义是:用户定义的 Shell 命令、HTTP 端点、MCP 工具调用、模型提示或子代理,在 Claude Code 生命周期的特定节点自动执行。
02 按时机选对事件
先判断需要在操作前拦截,还是在操作后处理结果,再选择事件:
| 阶段 | 常用事件 | 典型用途 |
|---|---|---|
| 会话开始与结束 | SessionStart、SessionEnd | 初始化环境、清理临时资源、记录会话 |
| 用户提交输入 | UserPromptSubmit | 注入动态背景、拒绝不合规输入 |
| 工具执行前 | PreToolUse、PermissionRequest | 检查命令、拦截操作、处理权限请求 |
| 工具执行后 | PostToolUse、PostToolUseFailure、PostToolBatch | 格式化、记录结果、归纳一批工具调用 |
| 回复结束 | Stop、StopFailure | 检查是否可以结束、记录 API 失败 |
| 等待用户 | Notification | 权限请求、完成等待等桌面通知 |
| 子代理 | SubagentStart、SubagentStop | 初始化代理资源、审计代理结果 |
| 配置与环境 | ConfigChange、InstructionsLoaded、CwdChanged、FileChanged | 审计配置、观察指令加载、刷新环境 |
| 上下文压缩 | PreCompact、PostCompact | 压缩前检查、压缩后记录摘要 |
需要阻止命令时,使用执行前的 PreToolUse;到 PostToolUse 时操作已经发生。Stop 只表示一次回复结束,不能据此宣布整个任务完成;用它阻止 Claude 结束时,要防止反复触发,做法见Claude Code 修复循环的五个停止条件。
03 第一层:确认已经注册
项目 Hook 在 .claude/settings.json 的 hooks 字段中注册,结构分三层:
hooks
└─ 事件,例如 PreToolUse
└─ Matcher 组,例如 Edit|Write
└─ hooks:实际运行的处理器Hook 可以放在不同范围:
| 位置 | 适用范围 |
|---|---|
~/.claude/settings.json | 你的所有项目,例如个人桌面通知 |
.claude/settings.json | 团队共享的当前项目自动化 |
.claude/settings.local.json | 当前项目中的个人试验或本机差异 |
| Managed Settings | 组织统一部署且不可由个人覆盖的策略 |
| Skill frontmatter | 调用后注册,持续到会话结束 |
| Agent frontmatter | 仅在该子代理运行期间生效 |
.claude/hooks/ 只存脚本,手动运行成功不代表已注册。普通 Hook 在 Settings 注册,插件使用 hooks/hooks.json。打开 /hooks,看它是否已注册,核对事件、来源和处理器,再触发事件。
04 第二层:Matcher 有没有匹配上
Matcher 负责挑出要处理的事件。它看什么,要看事件类型:可能是工具名、通知类型、代理名称,也可能是启动方式。
工具名可以用 | 或逗号连接多个精确值:
Edit|Write
Read, Grep, Glob想“只处理 Edit”,很容易写出 Edit.*。可 Matcher 遇到正则字符后,按 JavaScript 正则匹配,默认也不要求匹配完整名称,于是 NotebookEdit 也可能进来。Hooks 文档用的正是这个例子:Edit.* 同时匹配 Edit 和 NotebookEdit,需要整串匹配时,用 ^ 和 $ 包起来,写成 ^Edit$。
工具事件还可以在具体 handler 上使用 if,按权限规则语法进一步过滤工具参数:
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(git *)",
"command": "python3",
"args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/check-git.py"]
}
]
}示例中,Matcher 先挑出 Bash 调用,if 再看里面是不是 Git 命令,减少无关的脚本启动。若本意是禁止某个命令,还得写 permissions.deny。筛选处理器不能替代权限判断。
还有一类事件根本不看 Matcher。官方列出的包括 Stop、UserPromptSubmit、PostToolBatch 等,它们每次发生都会触发。先查事件接受什么字段,再填写过滤条件。Matcher 里的大小写也要核对,比如把 Edit 写成了 edit。排查配置的官方文档还列了两个“Hook 从不触发”的常见原因:Matcher 写成了 JSON 数组而不是字符串;在 v2.1.191 之前的版本里用逗号分隔,旧版本会把逗号当普通字符,"Edit,Write" 什么也匹配不到。
05 第三层和第四层:输入与结果
把最小事件 JSON 传给脚本,检查字段位置和边界输入。例如单独测试一个路径保护脚本:
printf '%s' '{"cwd":"/tmp/demo","tool_input":{"file_path":"/tmp/demo/.env"}}' \
| CLAUDE_PROJECT_DIR=/tmp/demo python3 .claude/hooks/protect-files.py
printf '%s\n' "$?"这样不经过 Claude Code,也不会实际写文件,就能看清脚本对各种输入的反应。
结果层要一起看退出码、stdout、stderr,检查所依赖的命令是否在系统中可用,判断到底拦住了,还是只报错后继续。退出码 1 在多数事件里只是不阻断的错误,这一点最容易误判,退出码和输出的完整约定见用 Hook 保护 .env 和密钥文件。
06 用日志代替猜测
要看 Claude Code 里实际怎样执行,用 claude --debug-file /tmp/claude-hooks.log 启动。CLI 参考文档说明,--debug-file 会把调试日志写到指定文件,并自动开启调试模式。日志能查匹配项、运行命令、退出码和两路输出。
配置问题先跑 /doctor;文件改了几秒后,/hooks 还没更新,再重启排除文件监视遗漏。statusMessage 可能一闪而过,没看到提示不能证明处理器没有运行。应以日志中的匹配、调用和结果为依据。
把四层排查连起来:
- 注册:打开
/hooks,看它是否已注册,核对事件、来源和处理器。 - 触发:查实际发生了哪个事件,看 Matcher 是否写对、大小写是否一致。
- 输入:把最小事件 JSON 传给脚本,检查字段位置和边界输入。
- 结果:一起看退出码、stdout、stderr 和依赖命令,判断拦住了还是只报错。
共享 Hook 要说明依赖、支持的系统及失败是否阻断,再用无害样本验证允许和拒绝。开头那个从没跑过的脚本,在 /hooks 里一看就知道:它只是放进了目录,还没登记。先定位层级,再改脚本,排查就不会在错的地方打转。