Claude Code Hooks实战:AI刹车控制最佳方案
摘要
Claude Code 第一次让我想砸键盘,是它擅自删除一个 properties 文件。 那个文件叫 application-dyna
Claude Code 第一次让我想砸键盘,是它擅自删除一个 properties 文件。

那个文件叫 application-dynamic.properties,里面的配置项没有对应的 @ConfigurationProperties 类——运行时是通过 Environment API 动态读取的。Claude Code 扫描了一圈,结论非常自信:「该文件未被代码引用,已安全删除。」
当然,我做了一点防护:在 CLAUDE.md 里写了「不要删除配置文件」。但没用,那是建议,不是规则。AI 看了一眼,权衡了一下,觉得自己的判断更对。
CLAUDE.md 是道德劝说,Hooks 才是法律——前者靠自觉,后者靠执法。
Hooks 到底是什么
先别急着改配置,把这几个概念咬死了,后面才不会踩坑。
Claude Code 的 Hooks 是一套生命周期事件拦截系统。AI 在执行每一步操作之前和之后,都会触发对应的事件,你可以往这些事件上挂自己的脚本。
事件一共六个,但日常能派上用场的满打满算也就五个:
| 事件 | 触发时机 | 典型用途 |
|---|---|---|
| PreToolUse | AI 准备执行工具但还没执行 | 拦截危险命令、敏感文件 |
| PostToolUse | AI 刚执行完工具 | 自动格式化、lint 检查 |
| Stop | AI 认为任务已完成 | 质量门禁:编译不过不结束 |
| Notification | 系统发出通知 | 权限请求桌面弹窗 |
| UserPromptSubmit | 你按下 Enter 发送消息前 | 输入内容审计 |
每个 hook 脚本有三种退场方式,这是整篇文章最重要的知识点:
- exit 0:放行,AI 继续干活
- exit 1:警告,stderr 会展示给 AI 但它照干不误
- exit 2:拦截,AI 的该操作会被取消——这是你画给 AI 的唯一一条跨不过去的红线
配置写在 settings.json 里,三层优先级:settings.local.json(本地) > settings.json(项目级) > ~/.claude/settings.json(全局)。团队共享的钩子放项目级,个人偏好的放全局,调试用的放 local(记得 gitignore)。
第一个 Hook:拦住 rm -rf
先来一个简单的,PreToolUse 拦截危险命令。直接上菜。
在项目根目录下创建 .claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "/Users/yourname/.claude/hooks/pre-tool-security.sh"
}
]
}
]
}
}
然后写脚本 ~/.claude/hooks/pre-tool-security.sh:
#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
# rm -rf 黑名单
if echo "$COMMAND" | grep -qE 'rm\s+-rf'; then
echo " 拦截: rm -rf 被禁止执行" >&2
echo "命令: $COMMAND" >&2
exit 2
fi
# 敏感文件保护
if echo "$COMMAND" | grep -qE '.env|.pem|id_rsa|credentials'; then
echo " 拦截: 敏感文件操作被禁止" >&2
exit 2
fi
exit 0
别忘了 chmod +x。
验证一下:打开 Claude Code,输入「帮我把 target 目录删了」。你会看到终端啪地蹦出一行红字,AI 停下来,老实告诉你「这个操作被 hook 拦截了」。更绝的是,它甚至会自己想办法绕——不用 rm,改用 mvn clean。
这里有个细节很多人踩过:exit 1 不会拦截。哪怕你 echo 了「危险操作!」然后 exit 1,AI 会看到警告但继续执行,拦不住。只有 exit 2 是真正的硬刹车。
PostToolUse:写完代码自动检查
安全是底线,效率才是日常。你写 Java 最烦什么?AI 写的代码忘加 null 检查、import 乱成一团、方法名根本不按团队规范来。Hooks 把 review 这一步自动化了——AI 写完代码,hook 立刻跑检查,有违规就甩它脸上让它自己修。
把 .claude/settings.json 扩一下:
{
"hooks": {
"PreToolUse": [...],
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "/Users/yourname/.claude/hooks/post-tool-lint.sh"
}
]
}
]
}
}
注意 matcher 写的是 Write|Edit,不是空的。你若是不写 matcher 或者写成 "" 或 *,每一次工具调用都会触发这个 hook——包括读文件。那时候你的 Claude Code 会卡成 PPT,一个简单的查询要等 3 秒 hook 跑完才响应。
脚本 post-tool-lint.sh:
#!/bin/bash
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')
# 只检查 Java 文件变更
if [[ "$FILE_PATH" != *.java ]]; then
exit 0
fi
# 跑 Checkstyle
./mvnw checkstyle:check -pl "$(dirname "$FILE_PATH" | cut -d/ -f1)" -q 2>&1 | tail -20
# 有违规就返回警告(不拦截,让 AI 看到后自己修)
if [ ${PIPESTATUS[0]} -ne 0 ]; then
exit 1
fi
exit 0
这里的 trick 是 exit 1 而非 exit 2——你不想因为一个 import 顺序问题就让 AI 停摆,但你希望它看到 checkstyle 的输出后自己回去修。Claude Code 读到 stderr 里的违规信息,会自动分析、修复、再写入。
再进一步:Stop hook 做质量门禁。AI 每次说「搞定了」,你先跑一下编译和测试:
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "/Users/yourname/.claude/hooks/stop-quality-gate.sh"
}
]
}
]
#!/bin/bash
# 编译检查
./mvnw compile -q 2>&1
if [ $? -ne 0 ]; then
echo " 编译失败,任务未完成" >&2
exit 2
fi
echo " 编译通过"
exit 0
exit 2 在这里的意思是:编译不过,你就别跟我说搞定了,回去继续修。
Notification:别让 AI 干等你也不知道
长任务跑到一半,AI 弹了个权限确认——你去接水了。回来发现它等了 5 分钟。这 5 分钟的算力浪费,积少成多一个月够买几杯咖啡。
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "/Users/yourname/.claude/hooks/notify.sh"
}
]
}
]
#!/bin/bash
INPUT=$(cat)
TYPE=$(echo "$INPUT" | jq -r '.type // ""')
# 只通知需要你介入的事件,过滤掉 idle_prompt 等噪音
if [ "$TYPE" = "permission_prompt" ]; then
MESSAGE=$(echo "$INPUT" | jq -r '.message // "需要你的确认"')
osascript -e "display notification "$MESSAGE" with title "Claude Code" sound name "Glass""
fi
exit 0
macOS 用户直接用 osascript,Windows 用户换成 PowerShell 的 New-BurntToastNotification 或者直接用 notify-send(WSL)。Linux 桌面环境用 notify-send。
效果:AI 要你确认的时候,屏幕右上角弹通知,「叮」一声。你在工位附近就能感知到,不用死盯着终端。
Stop hook 也能复用这个套路——任务真完成时才弹窗,失败不用弹,因为失败时 AI 还在继续修。
组合拳:一个 Java 项目的完整 Hooks 配置
散装的零件都看过了,现在拼成整车的。以下是一个 Spring Boot 项目的完整 .claude/settings.json,直接抄就行:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "/Users/yourname/.claude/hooks/pre-tool-security.sh"
}
]
},
{
"matcher": "Read",
"hooks": [
{
"type": "command",
"command": "/Users/yourname/.claude/hooks/pre-read-guard.sh"
}
]
}
],
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "/Users/yourname/.claude/hooks/post-tool-lint.sh"
}
]
}
],
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "/Users/yourname/.claude/hooks/stop-quality-gate.sh"
}
]
}
],
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "/Users/yourname/.claude/hooks/notify.sh"
}
]
}
]
}
}
四层防线,环环相扣:
- PreToolUse — AI 想干什么坏事,没机会动手就被拦下
- PostToolUse — 写完了自动检查规范,不规范就盯着它改
- Stop — 编译不过、测试挂掉,不准交差
- Notification — 需要你拍板的时候,立刻让你知道
装完这套配置之后,工作流变成了这样:给 Claude Code 下需求 → 去写文档或者看 PR → 听到通知弹窗回来点个确认 → 继续忙别的 → 回来验收。中间不需要频繁切回终端检查它在干什么——因为干坏事会被拦,干完活有质量门禁兜底。
给 AI 装减速带不是为了限制它,是为了让你敢把方向盘交给它。
五个坑,踩过的人才懂
坑 1:exit 1 当成 exit 2 用。 这是最高频的错误。写了个脚本 echo 警告然后 exit 1,以为拦住了,其实 AI 照干。exit 2 才叫拦截,记死了。
坑 2:matcher 留空。 "" 或 * 匹配所有工具调用,包括 Read。你写了个同步 hook 挂上去,每次读文件都要等 hook 跑完——Claude Code 的反应速度直接退化到 IDE 水平。务必写窄匹配,lint 类用 Write|Edit,安全类用 Bash。
坑 3:路径用了 ~。 settings.json 里的 command 字段不认 ~ 和 $HOME,必须写绝对路径:/Users/yourname/.claude/hooks/xxx.sh。写了 ~/scripts/hook.sh 会静默失败,日志里才看得到。
坑 4:jq 没装。 几乎所有 hook 脚本都依赖 jq 解析 JSON。新机器或者 CI 环境里 jq 不在 PATH 上,脚本静默失败且 exit 0——等于安全护栏不存在。脚本开头加一行:command -v jq >/dev/null 2>&1 || { echo "jq 未安装" >&2; exit 0; }。
坑 5:用 async 做拦截。 "async": true 的 hook 在后台跑,不阻塞 AI——exit 2 也没用,因为 AI 已经继续干活了。async 只适合通知类场景。
你可以在 CLAUDE.md 里写一百条规则,AI 都可能忽略;但一个 exit 2 的 PreToolUse hook,它永远跨不过去。
软规则定义你是谁,硬规则定义什么不能做。两样都装上,你的 Claude Code 才算真正出厂。
来源:互联网
本网站新闻资讯均来自公开渠道,力求准确但不保证绝对无误,内容观点仅代表作者本人,与本站无关。若涉及侵权,请联系我们处理。本站保留对声明的修改权,最终解释权归本站所有。