用了几个月 Claude Code,我发现一个规律:每次让 AI 自由发挥,总有那么一两件小事它会忘。格式化不一致、改到不该动的文件、跑完命令不通知我。
这些东西不该靠 prompt 去”请”它做——prompt 是软约束,它可能在第七轮对话后开始忽略。Hooks 是硬约束,每次都执行。
Claude Code 的 Hooks 在特定生命周期节点自动跑 shell 命令。编辑文件后、执行命令前、等待用户输入时、会话结束时——每个节点都能挂一段脚本。
我不是要翻译官方文档。这篇只记录我实际用上的几个 Hook,以及踩到的坑。
Hook 放在哪
两个位置:
| 位置 | 作用范围 | 是否提交 Git |
|---|---|---|
~/.claude/settings.json | 所有项目 | 否,本机 |
.claude/settings.json | 当前项目 | 是 |
我个人的习惯:格式化、通知这类通用 Hook 放 ~/.claude/settings.json。项目规则类(拦截危险命令、代码审查门禁)放项目的 .claude/settings.json,跟项目一起提交。
验证配置用 /hooks 命令,会列出所有已注册的 Hook 及触发次数。只读的,要改得回到 settings.json。
第一个 Hook:编辑后自动格式化
这是最直观的用法。Claude Code 编辑完文件后自动跑 prettier,代码风格永远一致,不需要在 prompt 里反复强调。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
]
}
}
注意这个 matcher: "Edit|Write"。不加的话每次工具调用完都跑,包括 Bash、Read 之类的——没必要。Edit|Write 只匹配文件编辑操作。
有个坑:Claude 也可以通过 Bash 工具用 echo > file 或 sed -i 改文件,PostToolUse 匹配不到这种。如果你需要覆盖所有文件变更,加一个 Stop Hook 在每轮结束扫描 git status --porcelain。不过我试下来,Edit/Write 覆盖了 95% 的场景,剩下的不值得多跑一个扫描。
拦截危险命令:PreToolUse
这是我最刚需的 Hook。已经发生过两次 Claude 顺手改了 .env 文件——不是故意的,但改了就是改了。
在项目根目录的 .claude/settings.json 加:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
}
]
}
]
}
}
脚本 protect-files.sh:
#!/bin/bash
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
PROTECTED_PATTERNS=(".env" "package-lock.json" ".git/")
for pattern in "${PROTECTED_PATTERNS[@]}"; do
if [[ "$FILE_PATH" == *"$pattern"* ]]; then
echo "Blocked: $FILE_PATH matches protected pattern '$pattern'" >&2
exit 2
fi
done
exit 0
关键点:
- exit 2 阻止操作。Claude 会收到你写在 stderr 里的原因,所以它能根据反馈调整方向。
- exit 0 放行。不需要决策时保持安静。
- 脚本要
chmod +x,不然跑不起来。
用了几周,.env 再也没被改过。Claude 被拦截后会自动换个方式——比如建议你自己手动改,或者问你要不要跳过保护。
桌面通知:切换窗口不用盯着终端
Claude Code 跑长时间任务时,我会切到别的窗口。但不知道它什么时候干完。Notification Hook 解决这个问题。
macOS 用的命令:
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
}
]
}
]
}
}
空的 matcher 对所有通知类型触发——权限请求、空闲等待、身份认证完成都弹通知。想更精确的话可以设成 idle_prompt,只在等用户输入时提醒。
macOS 上有个坑:osascript 的通知需要 Script Editor 有通知权限。第一次跑不会自动弹出权限请求,它静默失败。解决方法:先手动在终端跑一次 osascript -e 'display notification "test"',然后去 系统设置 > 通知 找到 Script Editor 并打开允许通知。之后再跑一次确认能看到通知弹窗。
Linux 用 notify-send,Windows 用 PowerShell。我在三台机器上都配了,很稳。
Stop Hook:每轮结果自动审查
这个比较进阶。Stop Hook 在 Claude 完成回复后触发。我用来做两件事:
1. 自动代码审查。 跑 lint、typecheck,不通过不允许结束。用的是 type: "agent",因为它要读文件、跑命令。
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "agent",
"prompt": "Review the code changes in this session. Check for: 1) unhandled errors, 2) security issues, 3) broken imports. If issues found, respond with {\"ok\": false, \"reason\": \"具体问题描述\"}. If clean, respond with {\"ok\": true}.",
"timeout": 120
}
]
}
]
}
}
"ok": false 时 Claude 会继续工作,用 reason 里的描述作为下一轮指令。
说实话,这个 Hook 在日常小任务上有点重——每改两行代码都要跑一次审查。我目前只在重要分支上启用它。
2. 压缩后重新注入上下文。 这个更实用。
{
"hooks": {
"SessionStart": [
{
"matcher": "compact",
"hooks": [
{
"type": "command",
"command": "echo 'Project rules: use Bun not npm. Tests must pass before commit.'"
}
]
}
]
}
}
SessionStart 匹配 compact 意味着每次对话被压缩后自动注入项目约定的摘要。注意:写固定的规则用 CLAUDE.md 就够了。这个 Hook 适合注入动态信息,比如 git log --oneline -5 把最近的 commit 历史喂进去。
三个踩坑记录
Hook 脚本被 shell 配置污染。 这是最坑的一次。我的 ~/.zshrc 里有一行 echo "Shell ready"——非交互式 shell 也会执行。Hook 跑脚本时这行被打到 stdout 里,跟脚本的 JSON 输出混在一起,Claude Code 解析 JSON 失败。
解决方法:把 echo 包在交互式检测里。
if [[ $- == *i* ]]; then
echo "Shell ready"
fi
PostToolUse 不能撤销操作。 工具已经执行完了,你只能记录或追加处理。要拦截在事前,用 PreToolUse。
Hook 默认超时 10 分钟。 对大多数场景够用,但 UserPromptSubmit 只有 30 秒。如果你在这个 Hook 里跑慢操作,指定 "timeout": 60。
实际效果
配完这几个 Hook 之后,几个收益:
- 代码格式不再需要口头提醒,每次编辑完自动处理
- 敏感文件自动保护,改不动
- 长时间任务结束有通知,切窗口不需要来回检查
- 压缩后项目约定不丢失
Hooks 和 prompt 的区别是:prompt 是跟 AI 商量,Hooks 是写进系统的规则。商量有时候管用,规则每次都管用。
下一步
Hooks 搞定了编译时和运行时的自动化。接下来想试试把 Claude Code 跟 OpenClaw 打通——Claude Code 写代码,OpenClaw 跑部署和监控。两个 Agent 协作,不只是一个。
系列文章和进度在 Vibe Coding 工作流。