完整解析 Claude Code Hooks 机制,从五种类型到退出码控制,配合四个真实配置案例,让 AI 操作可审计、可拦截、可自动化。
每次 Claude Code 读文件、写代码、执行命令,背后都有一套事件系统在运转。Hooks 就是接入这套系统的接口——你可以在任意时机注入自己的逻辑,自动做代码检查、记录日志、拦截危险操作,或者触发任何 shell 命令。
这篇文章把 Hooks 的机制、配置方式和实战用法完整讲一遍。
Hooks 是配置在 settings.json 里的 shell 命令,Claude Code 在特定事件发生时自动执行它们。
最直接的类比:git hooks。git 在 commit、push 等操作前后可以触发脚本,Claude Code Hooks 的思路完全一样,只是触发点是 AI 的工具调用。
为什么这很重要?
Claude Code 的能力越强,你越需要确定性的控制层。Hooks 提供的是:
- 不依赖 prompt 的执行保证(Claude 可能忽略指令,但 hook 一定跑)
- 可审计的操作记录
- 自动化的质量检查
| 类型 | 触发时机 | 典型用途 |
|---|---|---|
PreToolUse |
工具调用前 | 拦截危险操作、记录意图 |
PostToolUse |
工具调用后 | 自动 lint、运行测试 |
PreCompact |
上下文压缩前 | 保存当前状态快照 |
Notification |
Claude 发出通知时 | 桌面推送、Slack 消息 |
Stop |
Claude 完成回复时 | 汇总日志、触发后续流程 |
最常用的是 PreToolUse 和 PostToolUse,围绕工具调用做拦截和后处理。
Hooks 写在 ~/.claude/settings.json(全局)或项目根目录的 .claude/settings.json(项目级)里:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "npm run lint --silent"
}
]
}
]
}
}
三个核心字段:
matcher:匹配工具名的正则,决定这个 hook 在哪些工具调用后触发。"Write|Edit" 表示写文件或编辑文件时触发。留空或 ".*" 匹配所有工具。type:目前只有 "command"。command:任意 shell 命令。写 matcher 时需要知道工具叫什么名字:
| 工具名 | 对应操作 |
|---|---|
Write |
写入新文件 |
Edit |
编辑文件 |
Bash |
执行 shell 命令 |
Read |
读取文件 |
Glob |
文件查找 |
Grep |
内容搜索 |
TodoWrite |
更新任务列表 |
执行时有几个环境变量可用,通过 stdin 传入 JSON:
{
"tool_name": "Write",
"tool_input": {
"file_path": "/path/to/file.rb",
"content": "..."
},
"tool_response": "..."
}
PreToolUse 拿到 tool_input(调用前的入参),PostToolUse 同时拿到 tool_response(工具返回结果)。
可以用这些数据写有判断逻辑的 hook:
#!/bin/bash
input=$(cat)
file=$(echo "$input" | jq -r '.tool_input.file_path')
# 只对 .rb 文件运行 rubocop
if [[ "$file" == *.rb ]]; then
rubocop "$file" --autocorrect-all --no-color
fi
Hook 的退出码控制 Claude Code 的后续行为:
| 退出码 | 含义 |
|---|---|
0 |
成功,继续执行 |
2 |
阻断:取消当前工具调用,将 stderr 输出反馈给 Claude |
| 其他非零 | 记录错误,但继续执行 |
退出码 2 是最有用的——它让你可以在 PreToolUse 里写拦截逻辑,阻止 Claude 执行某个操作,并告诉它原因。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "bash -c 'file=$(echo \"$CLAUDE_TOOL_INPUT\" | jq -r .file_path 2>/dev/null); [[ \"$file\" == *.rb ]] && rubocop -A \"$file\" --no-color -q || true'"
}
]
}
]
}
}
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash -c 'cmd=$(echo \"$CLAUDE_TOOL_INPUT\" | jq -r .command); if echo \"$cmd\" | grep -qE \"rm.*/(migrations|seeds)\"; then echo \"禁止删除 migrations 或 seeds 目录\" >&2; exit 2; fi'"
}
]
}
]
}
}
当 Claude 尝试 rm -rf db/migrations/ 时,hook 返回退出码 2,Claude 收到错误信息,操作被取消。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit|Bash",
"hooks": [
{
"type": "command",
"command": "echo \"$(date '+%Y-%m-%d %H:%M:%S') $CLAUDE_TOOL_NAME: $(echo $CLAUDE_TOOL_INPUT | jq -c .)\" >> ~/.claude/audit.log"
}
]
}
]
}
}
每次文件写入或命令执行都记录到 ~/.claude/audit.log,方便回溯。
{
"hooks": {
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude 已完成\" with title \"Claude Code\"'"
}
]
}
]
}
}
长任务跑完自动通知,不用盯着终端。(macOS 用 osascript,Linux 用 notify-send)
两个位置可以放 Hooks:
全局(~/.claude/settings.json):适合所有项目通用的规则,比如日志、通知。
项目级(.claude/settings.json):适合项目专属的检查,比如这个项目用 rubocop,那个项目用 eslint。项目级 hooks 和全局 hooks 会合并执行,不会互相覆盖。
团队项目建议把项目级 settings.json 提交到 git,这样所有人都跑相同的 hooks。
Hook 不生效时的排查步骤:
write 不等于 Writeecho "hook triggered" >> /tmp/hook.log 确认触发,再加复杂逻辑Hooks 的核心价值是把不确定的 AI 行为和确定的工程规范接在一起。Claude 可能忘记运行测试,但 PostToolUse hook 不会。Claude 可能误删文件,但 PreToolUse 拦截不会放行。
从一个 hook 开始:写文件后自动 lint。跑顺了再加其他的。