DeepDojo.AI发布 2026-11-13更新 2026-11-13English
Claude Code Hook 该挂在哪个事件上
用了一段时间 Claude Code,你可能想给它加一道保险:它要执行 rm -rf 这类危险命令时,自动拦下来。Hook 就是做这件事的,它能在 Claude Code 工作流程中的特定时刻,运行你写的检查脚本。
很多人第一次写这种拦截,会把脚本挂到“工具执行完毕”的事件上。报错日志写得再详细、红字再醒目,磁盘上的文件也已经删掉了,它能做的只剩提醒你去翻备份。
同样叫“自动检查”,切入的时间点不同,效果天差地别:
- 代码刚写完落盘,可以挂一个 Hook 自动跑格式化,或者扫描有没有硬编码的 API 密钥;
- Bash 命令即将执行的前一刻,可以挂一个 Hook 检查参数,判断有没有破坏性指令;
- 用户刚提交提示词、Claude 还没开始处理时,可以做请求层面的预检。
所以写 Hook 的第一步是想清楚这次检查要改变什么:要阻止尚未发生的动作,就选执行前的事件;要查看已经产生的结果,就选执行后的事件。时机定了,再用 matcher 和 if 缩小范围、选合适的处理器类型。
01 先问检查要改变什么
选事件之前,先问问这次检查要改变什么:
想拦住尚未执行的命令
→ PreToolUse 匹配 Bash → 检查参数 → 返回拒绝
想查执行后的结果
→ PostToolUse → 反馈问题、提示补救相同的危险命令规则,放到 PostToolUse 时,删除等动作已经发生。Claude Code 的 Hooks 参考写得很直白:PostToolUse 在工具成功执行之后触发,这时返回的信息 Claude 能看到,但工具已经运行过了。阻止操作就选执行前事件,查看结果就选执行后事件。
主流程仍由 Skill、Agent 或人工组织,Hook 只在选定的事件上检查、记录、补信息或阻断。

02 按控制对象找事件
Claude Code 的 Hook 事件很多,可以先按控制对象分成四类:工具动作看 Tool Hooks(如 PreToolUse、PostToolUse、PermissionRequest),会话收尾看 Session Hooks(如 SessionStart、Stop、SubagentStop),用户输入看 UserPromptSubmit 这类任务事件,配置、目录、文件、上下文压缩和工作树的变化看生命周期事件。
最常用的几个事件如下:
| 事件 | 触发时机 | 常见用途 |
|---|---|---|
PreToolUse | Claude 生成工具参数后,工具实际执行前 | 阻断危险命令、修改工具输入、权限策略 |
PostToolUse | 工具成功执行后 | 格式化、扫描、记录日志、追加反馈 |
PostToolUseFailure | 工具执行失败后 | 错误记录、失败诊断、恢复建议 |
UserPromptSubmit | 用户提交 Prompt 后,Claude 处理前 | 校验用户请求、补充上下文、阻断危险请求 |
Stop | 主回复结束时 | 完成度检查、质量门禁 |
SubagentStop | 子代理结束时 | 检查子代理输出质量 |
SessionStart | 会话开始时 | 初始化日志、加载环境提示 |
SessionEnd | 会话结束时 | 清理、统计、最终日志 |
Notification | 出现通知时 | 权限提示、空闲提醒 |
PermissionRequest | 出现权限请求时 | 自动允许或拒绝特定请求 |
写 Hook 前先确定时机:阻断用执行前事件,检查结果用执行后事件,总结或收尾用 Stop、SubagentStop 或 SessionEnd。
SessionEnd 只在会话结束时触发,适合收尾记录,比如本次处理了哪些模块、遇到哪些问题、下次要继续什么;它无法阻止会话关闭,因此负责记录和清理,强制质量门禁要放到更早的可控节点。Stop 每轮回复结束都会触发,更适合轮次级检查。
03 配置放在哪里,脚本要先存在
Hook 脚本通常放在用户目录或项目目录中。配置可以放在三处:项目级 .claude/settings.json,本地个人的 .claude/settings.local.json,以及用户级 ~/.claude/settings.json。
最基本的配置结构如下:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/pre-tool-check.sh"
}
]
}
],
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/format-code.sh"
}
]
}
]
}
}配置里写了脚本路径,还要确认那个脚本已经存在。先保存、检查脚本,再加载配置,才有东西可执行。只建目录,行为不会变化。
接着想想两个检查有没有先后依赖。格式化和扫描都挂在同一事件上,看起来按数组排好了顺序,官方文档却说明:一个事件触发时,所有匹配的 Hook 并行运行。若扫描必须读取格式化后的文件,就让一个脚本依次调用它们,依赖关系才落实到了执行过程里。
04 matcher 选工具,if 看参数
matcher 用来匹配工具名或事件值。常见写法:
matcher | 含义 |
|---|---|
Write | 只匹配 Write 工具 |
Edit|Write | 匹配 Edit 或 Write |
Bash | 匹配 Bash 工具 |
* | 匹配所有工具 |
mcp__memory__.* | 匹配某个 MCP server 下的一组工具 |
先用 matcher 选工具,例如 Edit|Write 表示二选一;再用 if 看路径、参数或命令内容。if 接受一条权限规则,按权限规则的语法写过滤条件。例如,只检查 Write 对工作目录下 src/ 的写入:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/check-source-write.sh",
"if": "Write(src/**)"
}
]
}
]
}
}沿一次调用看这段配置,更容易分清两层条件:matcher 先选中 Edit 和 Write,处理器上的 if: "Write(src/**)" 再筛一次,最后只剩指定范围内的 Write。若以为前面的匹配已经覆盖 Edit,就会漏查。需要它时,再加一个 if: "Edit(src/**)" 的处理器。
if 与 type、command 同级。旧版本不支持这个字段时,把参数判断放进脚本。脚本拿到的文件路径是绝对路径,不能直接拿 ^src/ 去匹配,先解析路径,再判断是否落在目标目录。
假设 60 次调用中有 20 次 Bash,matcher 可以先排除其他工具。只查 Git 提交和推送时,分别设置 Bash(git commit *)、Bash(git push *) 两个处理器的 if。每项只写一条权限规则,用逗号拼接表示不了“或”。其他能写远端的命令,还要由权限和账号限制控制。
05 五类处理器按判断需要选
按判断需要选处理器。规则能写清楚的本地检查先用 command,需要服务、语义判断或调查时,再选其他类型:
| 类型 | 适合场景 |
|---|---|
command | 运行 shell 命令或脚本,适合本地检查、格式化、日志 |
http | 调用 HTTP 服务,适合通知、外部审计、远程策略 |
prompt | 让模型根据 Prompt 做判断,适合自然语言质量检查 |
mcp_tool | 调用 MCP 工具,适合接入外部工具和数据源 |
agent | 调用 agent,适合复杂判断或专项审查 |
拿提交说明检查来看:
| 标准 | 处理方式 |
|---|---|
| 首行长度、类型前缀、空行 | command 脚本 |
| 有没有解释修改原因 | prompt 判断,留意成本和边缘结果 |
| 两者都需要 | 一个流程先查格式,通过后再查含义 |
Command Hook 最常见:
{
"type": "command",
"command": "python3 ~/.claude/hooks/security-scan.py",
"timeout": 30
}timeout 限制执行时间。每次动作匹配都会运行 Hook,处理逻辑尽量简短,减少工具调用的等待时间。Prompt Hook 常用于 Stop 或 SubagentStop,检查“这个任务是否完成”“输出是否满足要求”;Agent Hook 适合更复杂的审查,比如让安全审查 agent 检查某个子代理的输出。
06 脚本从 stdin 读事件信息
Claude Code 会把事件信息以 JSON 形式传给 Hook。不同事件字段略有差异,常见字段包括:
| 字段 | 含义 |
|---|---|
session_id | 会话 ID |
transcript_path | 会话 transcript 文件路径 |
cwd | 当前工作目录 |
permission_mode | 当前权限模式 |
hook_event_name | 触发事件名 |
tool_name | 工具名称 |
tool_input | 工具输入参数 |
tool_use_id | 工具调用 ID |
在写真正的解析脚本前,先挑准你想抓的事件,搞清楚它给了哪些字段。比如工具调用会带 tool_input,普通的会话生命周期事件就没有这个字段。先用不含敏感信息的合成 JSON 验证解析逻辑,再用真实事件核对必要字段。
脚本一般从 stdin 读取 JSON:
#!/usr/bin/env bash
set -euo pipefail
INPUT="$(cat)"
EVENT="$(printf '%s' "$INPUT" | jq -r '.hook_event_name // ""')"
TOOL_NAME="$(printf '%s' "$INPUT" | jq -r '.tool_name // ""')"
echo "event=$EVENT tool=$TOOL_NAME" >> "$HOME/.claude/hook-debug.log"本地没有 jq 时,也可以用 Python 的 json.load(sys.stdin) 解析。
07 写入后格式化:一个 PostToolUse 例子
自动格式化要读取写入后的文件,因此放在 PostToolUse,matcher 为 Write|Edit。脚本根据文件扩展名选择工具:
#!/usr/bin/env bash
set -euo pipefail
INPUT="$(cat)"
FILE_PATH="$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // ""')"
if [ -z "$FILE_PATH" ] || [ ! -f "$FILE_PATH" ]; then
exit 0
fi
case "$FILE_PATH" in
*.js|*.jsx|*.ts|*.tsx|*.json|*.css|*.md)
if command -v prettier >/dev/null 2>&1; then
prettier --write "$FILE_PATH" >/dev/null || { echo "Formatter failed; inspect the file." >&2; exit 1; }
fi
;;
*.py)
if command -v black >/dev/null 2>&1; then
black "$FILE_PATH" >/dev/null || { echo "Formatter failed; inspect the file." >&2; exit 1; }
fi
;;
*.go)
if command -v gofmt >/dev/null 2>&1; then
gofmt -w "$FILE_PATH" >/dev/null || { echo "Formatter failed; inspect the file." >&2; exit 1; }
fi
;;
esac
exit 0这段脚本有个需要看清的选择:缺少 formatter 时直接跳过,工具运行失败时才报错。因此“Hook 没报错”可能只表示本机没有那个工具。若项目要求格式必须通过,就在提交或 CI 阶段再运行确定性检查,这里的跳过只说明没检查过。
成对的 Hook 也能做轮次级统计,比如在 UserPromptSubmit 和 Stop 各记一次会话文本长度,看哪一轮异常冗长。这只是排查线索:想看当前上下文还剩多少,直接用 /context,文本增量换算不出 token、费用或剩余空间。
08 组件级 Hook、MCP 匹配和上线前检查
除了全局设置,Skill、子代理等组件也可以声明自己的 Hooks,例如安全操作 Skill 在 frontmatter 中声明:运行 Bash 前先执行一次检查脚本。组件级 Hook 的作用域更窄,只影响对应组件,适合局部质量门禁。对子代理来说,frontmatter 里写的 Stop Hook 会在运行时转换为 SubagentStop,只在该子代理结束时触发。
MCP 工具也可以用 matcher 匹配,形式通常是 mcp__<server>__<tool>,例如 mcp__memory__.* 匹配 memory server 下的一组工具。MCP Tool Hooks 可以记录外部调用、限制敏感工具或增加审计。设计时先收紧权限边界:外部系统可能连接数据库、工单、文件服务或部署平台,一次误操作的影响范围更大。
共享给团队前,核对这些安排:
| 检查面 | 做法 |
|---|---|
| 职责和耗时 | 一项 Hook 做一类短检查,设置超时 |
| 阻断和副作用 | 规则明确,测试正常任务是否被误伤 |
| 凭据和日志 | 不硬编码密钥,记录前脱敏 |
| 上线使用 | 先本地验证,再放项目配置;写清拦什么、记什么、改什么 |
共享前可以数一数:一次普通编辑会触发几次检查,每次要等多久?前置 Hook 每次调用都要等它跑完,经验上单次控制在半秒左右,开发者的思路才不容易被打断。
规则能明确写出的,就用短小的命令脚本。确实需要语义判断,再考虑 Prompt 或 Agent,承担相应成本和不确定性。长流程仍交给 Skill 或 Subagent,避免一次工具调用就带出一轮完整审查。
时机和匹配选对了,门禁仍可能失效:脚本自己崩溃时调用照常放行、字符串黑名单被换个写法绕过、Stop 检查陷入循环。这些问题和测试方法见 Claude Code Hook 为什么没拦住。斜杠命令里的 allowed-tools 只负责免确认,真正在动作前拦截要靠 Hook 或权限规则,两者的区别见Claude Code 只读命令为什么还能改文件。
选事件的顺序可以压成三步:先问检查要改变什么,再定在执行前还是执行后,最后才轮到 matcher、if 和处理器类型。