Claude Code 本质是概率性的——同一提示跑两次结果可能不同。但 Hooks(钩子) 是其中罕见的确定性机制:由 harness 在固定生命周期点强制执行 shell 命令,模型无法绕过。本教程带你用 PreToolUse、PostToolUse、UserPromptSubmit、Stop 四类钩子构建一套"自动拦截危险命令 + 自动格式化 + 自动注入上下文 + 完工通知"的护栏系统。适合已熟悉 Claude Code 日常用法、想把它放进团队/生产流水线的开发者。
package.json 的前端项目(用于演示 Prettier 自动格式化),或任意 Python 项目(演示 black)settings.json 的三层优先级:项目 .claude/settings.json > 用户 ~/.claude/settings.jsonHooks 不是提示词,也不是 Skill,而是由 harness 在工具调用生命周期里插入的 shell 命令。关键在于:它们由 harness 执行,模型自己改不了、也跳不过。这正是它比"在 CLAUDE.md 里写一句『别删数据库』"可靠得多的根本原因。
四类最常用的钩子事件:
| 钩子事件 | 触发时机 | 典型用途 | 能否阻断流程 |
|---|---|---|---|
PreToolUse |
工具执行前 | 拦截危险命令、保护敏感文件、二次确认 | 能(deny/block) |
PostToolUse |
工具执行后 | 自动格式化、跑 lint、记录日志 | 否(只做副作用) |
UserPromptSubmit |
用户提交提示词后、送模型前 | 注入上下文、拦截敏感提问 | 能(exit 2) |
Stop |
模型结束本轮回复 | 完工通知、自动跑测试、归档 | 能(让模型继续) |
每个钩子执行时,harness 会通过 stdin 喂一段 JSON,包含 session_id、transcript_path、cwd、hook_event_name,以及工具相关的 tool_name / tool_input / tool_response。你的脚本读这段 JSON 做判断,再通过退出码或 stdout 的 JSON 反向控制 harness。
输入 /hooks 回车,能看到一个交互式管理界面,列出当前生效的所有钩子及其来源(user / project / local)。这就是你后续所有配置的"真值表"。
rm -rf",模型偶尔还是执行。PreToolUse 钩子做硬拦截。这是整个护栏系统的心脏。在项目根目录创建 .claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "bash .claude/hooks/guard-bash.sh" }
]
},
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{ "type": "command", "command": "bash .claude/hooks/guard-write.sh" }
]
}
]
}
}
matcher 是正则,对工具名做匹配,多条 matcher 之间是或关系。注意 Write|Edit|MultiEdit 必须用一条正则覆盖三种写入工具,漏掉 MultiEdit 是最常见的配置漏洞。
创建 .claude/hooks/guard-bash.sh:
#!/usr/bin/env bash
# 读取 harness 注入的 JSON
input=$(cat)
cmd=$(echo "$input" | jq -r '.tool_input.command // ""')
# 1. 拦截递归删除根目录/家目录
if echo "$cmd" | grep -qE 'rm\s+-rf?\s+(/|~|\$HOME|\*)'; then
echo "⛔ BLOCKED: 检测到递归删除根/家目录命令: $cmd" >&2
exit 2
fi
# 2. 拦截强制推送主分支
if echo "$cmd" | grep -qE 'git\s+push.*--force.*main|git\s+push.*-f.*master'; then
echo "⛔ BLOCKED: 禁止强制推送主分支" >&2
exit 2
fi
# 3. 拦截管道执行远程脚本
if echo "$cmd" | grep -qE 'curl.*\|\s*(sh|bash)|wget.*\|\s*(sh|bash)'; then
echo "⛔ BLOCKED: 禁止管道执行远程脚本" >&2
exit 2
fi
# 放行
exit 0
控制协议要记牢两条:退出码 2 = 阻断工具并把 stderr 回喂给模型(模型会"看到"被拦并改道);退出码 0 = 放行。退出码 2 是最常用、最可靠的阻断方式,几乎所有版本都兼容。
当模型尝试 rm -rf /,你会看到红色拦截信息,工具根本不会执行,模型随后会自行道歉并换方案。日志里看不到任何 rm 真实执行痕迹。
jq 是否安装(钩子静默失败常因缺 jq);② matcher 写成 "bash" 而非 "Bash"——大小写敏感,工具名首字母大写;③ /hooks 里看不到该条,说明 settings.json 路径或 JSON 语法错了。exit 2 前输出 建议改用 git revert <commit>,模型会照做。PreToolUse 负责"不让坏事发生",PostToolUse 负责"让好事自动发生"。配置:
"PostToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{ "type": "command", "command": "bash .claude/hooks/auto-format.sh" }
]
}
]
.claude/hooks/auto-format.sh:
#!/usr/bin/env bash
input=$(cat)
file=$(echo "$input" | jq -r '.tool_input.file_path // ""')
[ -z "$file" ] || [ ! -f "$file" ] && exit 0
case "$file" in
*.js|*.ts|*.jsx|*.tsx|*.json|*.css)
npx --no-install prettier --write "$file" 2>/dev/null ;;
*.py)
python -m black "$file" 2>/dev/null
python -m isort "$file" 2>/dev/null ;;
*.go)
gofmt -w "$file" 2>/dev/null ;;
esac
exit 0
PostToolUse 的 stdin 还多了 tool_response,你可以据此判断工具是否成功。注意钩子始终在项目根目录执行(cwd 固定),所以脚本里用相对路径引用工具时要小心,引用文件本身则用 stdin 给的绝对 file_path。
模型每次 Edit 完一个文件,下一次 Read 或 git diff 时,代码已经自动按 Prettier/Black 规范排好版。你不再需要手动 npx prettier --write .,也不用在 CLAUDE.md 里反复叮嘱"写完要格式化"——它每次都执行,零遗漏。
PostToolUse 改文件发生在工具返回之后,模型的上下文里还是改之前的版本。Read 一次。不要试图在钩子里改模型上下文——钩子是副作用通道,不是对话通道。这是最被低估的钩子。它在用户回车之后、送给模型之前触发,能在不修改用户输入的前提下追加上下文,或拦截不合规提问。
"UserPromptSubmit": [
{
"hooks": [
{ "type": "command", "command": "bash .claude/hooks/enrich-prompt.sh" }
]
}
]
.claude/hooks/enrich-prompt.sh:
#!/usr/bin/env bash
input=$(cat)
prompt=$(echo "$input" | jq -r '.prompt // ""')
# 拦截:提示词里带真实密钥
if echo "$prompt" | grep -qE 'sk-ant-[A-Za-z0-9]{20,}|AKIA[A-Z0-9]{16}'; then
echo "⛔ 检测到提示词含疑似 API 密钥,已拒绝提交。请先脱敏。" >&2
exit 2
fi
# 注入:当前 git 分支与未提交文件数,让模型始终知晓上下文
branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "detached")
dirty=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
cat <<EOF
{
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": "[护栏注入] 当前分支: $branch | 未提交改动: $dirty 个文件。修改前请确认是否需要先 stash。"
}
}
EOF
exit 0
这里用到了 JSON stdout 控制:返回 hookSpecificOutput.additionalContext 会被 harness 追加到这一轮上下文里,用户看不到但模型看得到。退出码 2 则拦截整次提交,stderr 提示给用户。
你问"帮我把这个函数重构一下",模型在动手前会自己先确认分支、先提示有未提交改动——因为它每次都被注入了 git 状态。你不写一行 CLAUDE.md 就获得了"团队级上下文感知"。
additionalContext 要短、结构化、按需。本例只两行。别注入整个 README。UserPromptSubmit 的拦截要保守,只挡确定性危险(明文密钥、生产库 DDL),别用模糊正则误伤。Stop 在模型结束本轮、把控制权交还用户时触发。最常见的两种用法:桌面通知(长任务跑完提醒)和自动跑测试(每次完工自检)。
"Stop": [
{
"hooks": [
{ "type": "command", "command": "bash .claude/hooks/on-stop.sh" }
]
}
]
.claude/hooks/on-stop.sh:
#!/usr/bin/env bash
input=$(cat)
# Stop hook 若返回 {"decision":"block","reason":"..."},模型会被迫继续,适合"没跑测试不许停"
# 这里只做通知,不阻断
os=$(uname -s)
if [ "$os" = "Darwin" ]; then
osascript -e 'display notification "Claude 已完成本轮任务" with title "Claude Code" sound name "Glass"'
else
# Linux: 通用终端响铃 + 可选 libnotify
printf '\a'
command -v notify-send >/dev/null && notify-send "Claude Code" "已完成本轮任务"
fi
exit 0
进阶用法:如果想让"未通过测试就不许停",可在脚本末尾返回:
if ! npm test --silent >/dev/null 2>&1; then
echo '{"decision":"block","reason":"测试未通过,请修复后再结束。"}'
# 模型会继续工作直到测试绿
fi
注意 Stop 的 block 是强约束但有风险——若测试环境本身坏掉,模型会陷入无限循环。务必设超时与重试上限。
长任务(重构、批量改造)跑完,桌面弹出通知;如果你启用了 block 模式,模型在测试转绿前根本"停不下来",相当于一个免费的 QA 把关人。
osascript 才能发声。把上面四个钩子组装成一个可分发的项目级护栏套件,目录结构:
.claude/
├── settings.json # 钩子注册
└── hooks/
├── guard-bash.sh # PreToolUse: 拦危险命令
├── guard-write.sh # PreToolUse: 保护 .env/密钥
├── auto-format.sh # PostToolUse: 自动格式化
├── enrich-prompt.sh # UserPromptSubmit: 注入+拦截
└── on-stop.sh # Stop: 通知+质检
guard-write.sh 补上文件保护:
#!/usr/bin/env bash
input=$(cat)
file=$(echo "$input" | jq -r '.tool_input.file_path // ""')
# 禁止写入/覆盖敏感文件
case "$file" in
*.env|.env*|*secrets*|*credentials*|id_rsa|*.pem)
echo "⛔ BLOCKED: 禁止直接修改敏感文件: $file" >&2
exit 2 ;;
esac
# 禁止写入项目根之外的路径(防越权)
proj_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
case "$file" in
"$proj_root"/*) exit 0 ;; # 在项目内,放行
*) echo "⛔ BLOCKED: 禁止写入项目目录之外: $file" >&2; exit 2 ;;
esac
完整的 settings.json:
{
"hooks": {
"PreToolUse": [
{ "matcher": "Bash",
"hooks": [{ "type": "command", "command": "bash .claude/hooks/guard-bash.sh" }] },
{ "matcher": "Write|Edit|MultiEdit",
"hooks": [{ "type": "command", "command": "bash .claude/hooks/guard-write.sh" }] }
],
"PostToolUse": [
{ "matcher": "Write|Edit|MultiEdit",
"hooks": [{ "type": "command", "command": "bash .claude/hooks/auto-format.sh" }] }
],
"UserPromptSubmit": [
{ "hooks": [{ "type": "command", "command": "bash .claude/hooks/enrich-prompt.sh" }] }
],
"Stop": [
{ "hooks": [{ "type": "command", "command": "bash .claude/hooks/on-stop.sh" }] }
]
}
}
部署后效果:任何 teammate cd 进这个项目,Claude Code 立刻"穿上护甲"——危险命令挡掉、敏感文件锁死、代码自动排版、提示词自带 git 上下文、完工自动通知。整套配置随仓库走,无需逐人培训。
rm -rf node_modules(应放行)vs rm -rf /(应阻断)——验证正则颗粒度.env(应阻断)vs .env.example(看你正则怎么定)——验证白名单app.tsx 后 git diff 应已格式化——验证 PostToolUseflowchart TD
U[用户输入提示词] --> UPS{UserPromptSubmit<br/>钩子}
UPS -->|exit 2 拦截| UR[拒绝并提示用户]
UPS -->|exit 0 + 注入 ctx| M[送入模型]
M -->|决定调用工具| PTU{PreToolUse<br/>钩子}
PTU -->|exit 2 阻断| BLK[工具不执行<br/>原因回喂模型]
PTU -->|exit 0 放行| RUN[工具真实执行]
RUN --> PTU2{PostToolUse<br/>钩子}
PTU2 -->|格式化/lint| SIDE[副作用: 改文件/记录]
SIDE --> M2[模型继续推理]
M2 -->|本轮结束| STOP{Stop 钩子}
STOP -->|通知| NTF[桌面/终端通知]
STOP -->|decision block| M2B[强制继续: 未过测试不许停]
STOP -->|放行| DONE[交还控制权给用户]
整条链路的关键在于:模型永远在"被护栏包围"的沙箱里工作。PreToolUse 是入口闸门,PostToolUse 是出口美化,UserPromptSubmit 是输入增强,Stop 是出口质检——四者闭环,构成确定性护栏。
exit 2 跨版本最稳,能用退出码解决的就别上 JSON。需要细粒度控制(如 permissionDecision: ask 走二次确认弹窗)才用 JSON stdout。Bash 不是 bash,Write|Edit|MultiEdit 漏 MultiEdit 等于漏一半写入。jq、脚本没 chmod +x、路径错——harness 不会报错,钩子直接"消失"。务必 /hooks 里逐条确认,并给每个脚本加 set -euo pipefail 之外的宽松默认放行(catch-all exit 0),避免护栏自身把正常工作流卡死。~/.claude/settings.json 放通用偏好,项目级放团队护栏,.claude/settings.local.json 放个人实验。项目级会覆盖而非合并同名 matcher,部署前理清优先级。exit 2 都在 stderr 给出为什么 + 怎么改,模型才能优雅降级,而不是硬刚死循环。code.claude.com/docs —— 退出码、JSON schema、全部 9 个钩子事件的权威定义/hooks 交互命令 —— 不用改 JSON 就能临时增删启停钩子,调试利器allow/deny/ask 三态,ask 会弹原生确认框,适合"高危操作要人确认"场景jq 和正则。guard-bash.sh 规则,并在课堂上"攻防演练"——一人当模型尝试破坏,一人当护栏,看谁先被拦。rm -rf、PostToolUse 接 Prettier、保护 .env 的确定性护栏实战,本文拦截与格式化脚本的主要范式来源。[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。
