本教程带你掌握 Claude Code 的 Hooks(生命周期钩子) 系统——一种在工具执行、对话轮次、会话启停等关键节点自动触发用户脚本的能力。学完后你将能够:用 PreToolUse 在危险命令执行前拦下它、用 PostToolUse 在文件写入后自动格式化、用 UserPromptSubmit 给每条提问注入项目上下文、用 Notification 在 Claude 等待确认时桌面提醒。Hooks 是把 Claude Code 从"陪你聊天的助手"变成"受控的自动化执行体"的核心机制,适合已有基础配置经验、想让 Agent 真正无人值守且安全可控的开发者。
CLAUDE.md 与 settings.json 的基本配置,了解权限(permissions)概念Hooks 不是单一触发点,而是分布在 Claude Code 会话全生命周期的一组事件。按触发频率,它们归为三种节奏(cadence),记住这个分类,是设计护栏的前提:
flowchart TD
S([会话开始]) --> SS[SessionStart<br/>每会话一次]
SS --> Loop[进入对话轮次循环]
Loop --> UPS[UserPromptSubmit<br/>每轮提问一次]
UPS --> Agent[Agent 工具调用循环]
Agent --> Pre{PreToolUse<br/>每次工具调用前}
Pre -->|allow/放行| Exec[工具执行]
Pre -->|deny/拦截| Block[阻断并反馈]
Exec --> Post{PostToolUse<br/>每次工具调用后}
Post --> Side[副作用:格式化/日志/校验]
Side --> More{还有工具调用?}
More -->|是| Pre
More -->|否| Stop[Stop<br/>本轮回答结束]
Block --> Stop
Stop --> Loop
Loop --> End([会话结束])
End --> SE[SessionEnd<br/>每会话一次]
SessionStart、SessionEnd。适合初始化环境、加载技能、收尾清理。UserPromptSubmit(用户提交提问时)、Stop(Claude 结束本轮回答时)、StopFailure。PreToolUse、PostToolUse、PostToolUseFailure、PostToolBatch。这是护栏的核心战场。此外还有一批异步事件(async events),如 Notification、PreCompact、PostCompact、SubagentStart、SubagentStop、TaskCreated、TaskCompleted、ConfigChange、InstructionsLoaded、CwdChanged、FileChanged。它们不阻塞主流程,仅产生副作用(发通知、写日志)。
预期结果:在脑海里建立"会话→轮次→工具调用"的三层嵌套视图,知道每类钩子挂在哪一层。
Claude Code 与你的钩子脚本之间有四条通道,理解它们的协议是写出可靠钩子的关键:
| 通道 | 方向 | 用途 |
|---|---|---|
| stdin | Claude Code → 脚本 | 传入事件上下文(JSON),含工具名、参数、会话 ID、cwd 等 |
| stdout | 脚本 → Claude Code | 传回 JSON 结构化决策(仅在 exit 0 时被解析) |
| stderr | 脚本 → 用户 | 反馈信息,exit 2 时会展示给用户 |
| exit code | 脚本 → Claude Code | 0 放行、2 阻断、1 非阻断错误 |
关键陷阱:Claude Code 仅在退出码为 0 时解析 stdout 中的 JSON;若 exit 2,任何 JSON 都被忽略。这与传统 Unix 习惯相反——exit 1 被当作"非阻断错误",Claude Code 照常继续执行动作。所以"想阻止一个动作",你必须用 exit 2 或 exit 0 + JSON permissionDecision: "deny",绝不能用 exit 1。
一个最小可运行的钩子脚本(读取 stdin、打印决策)长这样:
#!/usr/bin/env bash
# 读取 Claude Code 经 stdin 传入的事件 JSON
input=$(cat)
# 提取工具名(用 jq 解析)
tool_name=$(echo "$input" | jq -r '.tool_name // empty')
if [ "$tool_name" = "Bash" ]; then
cmd=$(echo "$input" | jq -r '.tool_input.command // empty')
# 检测危险命令
if echo "$cmd" | grep -qE 'rm -rf /|sudo '; then
echo "检测到危险命令: $cmd" >&2
exit 2 # exit 2 = 阻断
fi
fi
exit 0 # exit 0 = 放行
常见问题:脚本没有 #!/usr/bin/env bash 头、没有执行权限(chmod +x)、依赖 jq 但环境未安装——这三点占了新手 90% 的失败原因。可先用 claude --debug 查看钩子的实际调用与报错。
钩子配置写在 settings.json 的 hooks 字段下。settings.json 存在三个层级,按优先级从高到低为:企业策略 > 用户级(~/.claude/settings.json)> 项目级(.claude/settings.json)。一个事件下可挂多条钩子,按顺序执行:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/guard-bash.sh"
}
]
},
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/no-secrets.sh"
}
]
}
],
"PostToolUse": [
{
"matcher": "Write",
"hooks": [
{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/autoformat.sh" }
]
}
]
}
}
注意新版结构是 事件名 → [ {matcher, hooks:[{type, command}]} ]。type 目前支持 command(shell 脚本)、http(HTTP 端点)、prompt(LLM 提示)。$CLAUDE_PROJECT_DIR 是 Claude Code 注入的项目根环境变量,钩子里随时可用。
Matcher(匹配器) 决定钩子是否触发,规则因包含字符而异:纯工具名(如 Bash)精确匹配;含 | 表示多工具(Edit|Write);含正则元字符则按正则解析,例如 mcp__.*__write.* 能匹配所有 MCP 服务器下名字含 write 的工具,mcp__memory__.* 匹配 memory 服务器的全部工具。留空 matcher 的钩子对该事件每次都触发。
预期结果:配置写入后重启会话,对一个 Write 操作应能看到对应 PostToolUse 钩子被调用(在 claude --debug 日志里)。
PreToolUse 在工具执行之前触发,且无论当前权限状态如何都会运行——这意味着它是你最后一道确定性防线。用退出码只能"阻断或沉默",而用 JSON 决策可以实现更精细的控制。在 exit 0 时向 stdout 打印结构化 JSON:
#!/usr/bin/env bash
input=$(cat)
tool_name=$(echo "$input" | jq -r '.tool_name')
cmd=$(echo "$input" | jq -r '.tool_input.command // ""')
# 规则:禁止任何 force push 与 main 分支操作
if echo "$cmd" | grep -qE 'git push .* --force|git push -f|git checkout main|git reset --hard'; then
cat <<EOF
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "禁止强制推送与对 main 的破坏性操作(由护栏钩子拦截)"
}
}
EOF
exit 0
fi
exit 0
permissionDecision 三种取值:
"deny":阻断该工具调用,把 reason 反馈给 Claude,它会调整策略重试。"allow":显式放行,并可作为"自动批准但有安全网"模式——配合较宽松的权限策略,让钩子做守门员。"defer":交还给正常权限流程判断(不覆盖默认行为)。当同一事件挂了多个 PreToolUse 钩子且返回不同决策时,有明确的优先级规则(deny 通常优先于 allow),官方文档对此有完整定义,复杂场景务必查阅"PreToolUse decision control"小节确认。
常见问题:返回了 permissionDecision: "deny" 但 Edit 工具仍被执行——这通常是 JSON 格式错误(多余逗号、未转义引号)导致解析失败而回退为放行。务必用 jq -n 构造 JSON 而非手拼字符串,避免命令中的特殊字符破坏结构。
PostToolUse 在工具执行之后触发,最适合副作用类工作:自动 lint、格式化、运行类型检查、写审计日志。它不能撤销已发生的操作,但可以在文件落盘后立即修正。
一个典型的"写完即格式化"钩子:
#!/usr/bin/env bash
input=$(cat)
file_path=$(echo "$input" | jq -r '.tool_input.file_path // empty')
# 仅处理项目内的源码文件
case "$file_path" in
*.js|*.ts|*.jsx|*.tsx)
# 自动用项目配置格式化
npx prettier --write "$file_path" >/dev/null 2>&1
# 运行类型检查,失败则反馈给 Claude
if ! npx tsc --noEmit "$file_path" >/dev/null 2>&1; then
echo "类型检查未通过,请修复 $file_path" >&2
fi
;;
*.py)
ruff format "$file_path" >/dev/null 2>&1
ruff check --fix "$file_path" >/dev/null 2>&1
;;
esac
exit 0
PostToolUse 同样支持 JSON 决策控制(PostToolUseFailure 处理工具调用失败的情况),并能在 hookSpecificOutput 里返回额外上下文。由于它在每次工具调用后都跑,务必让脚本快速返回(毫秒级),否则会拖慢整个 Agent 循环。重活(全量测试套件)留给 Stop 钩子。
UserPromptSubmit 在用户按下回车、Claude 看到提问之前触发。它最强大的用法是为每条提问附加额外上下文,无需用户手动维护。这比把所有背景塞进 CLAUDE.md 更动态——可以根据当前 git 分支、时间、文件状态实时生成。
#!/usr/bin/env bash
branch=$(git branch --show-current 2>/dev/null)
uncommitted=$(git status --porcelain 2>/dev/null | wc -l)
today=$(date +%Y-%m-%d)
# exit 0 + additionalContext 注入到提问上下文
cat <<EOF
{
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": "[自动注入] 当前分支: ${branch} | 未提交变更: ${uncommitted} 个文件 | 日期: ${today}。回答请考虑当前分支上下文。"
}
}
EOF
exit 0
注意:UserPromptSubmit 用 exit 0 + JSON 的 additionalContext 来补充上下文;若想直接拦截/拒绝某条提问,则用 exit 2(stderr 反馈给用户,提问不会送达 Claude)。这能实现"工作时间外禁止提问"或"提问必须包含任务编号"等软约束。
当 Claude 需要你确认权限、或长时间空闲时,它会触发 Notification 事件。配置一个钩子把通知转发到桌面/手机,你就无需一直盯着终端。macOS 示例:
{
"hooks": {
"Notification": [
{
"matcher": "permission_prompt",
"hooks": [
{ "type": "command", "command": "osascript -e 'display notification \"Claude 需要确认权限\" with title \"Claude Code\" sound name \"Glass\"'" }
]
}
]
}
}
matcher 在 Notification 事件上按通知类型过滤(如 permission_prompt、idle_prompt)。Notification 钩子是纯副作用,不能阻断或修改通知本身。在 Linux 上可换成 notify-send,或转发到 Slack/钉钉 webhook 实现移动办公。
让我们把上述能力组装成一个真实可用的护栏体系。目标项目是一个 Web 服务,Claude Code 负责日常开发,但必须满足:
rm -rf 根目录、git push --force 等破坏性命令项目目录结构:
my-service/
├── .claude/
│ ├── settings.json
│ └── hooks/
│ ├── guard-bash.sh
│ ├── no-secrets.sh
│ ├── autoformat.sh
│ ├── inject-context.sh
│ └── notify.sh
├── CLAUDE.md
└── src/
.claude/settings.json(项目级配置,随仓库分发,团队共享护栏):
{
"hooks": {
"PreToolUse": [
{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/guard-bash.sh" }] },
{ "matcher": "Write|Edit|MultiEdit", "hooks": [{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/no-secrets.sh" }] }
],
"PostToolUse": [
{ "matcher": "Write|Edit", "hooks": [{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/autoformat.sh" }] }
],
"UserPromptSubmit": [
{ "hooks": [{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/inject-context.sh" }] }
],
"Notification": [
{ "matcher": "permission_prompt", "hooks": [{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/notify.sh" }] }
]
}
}
no-secrets.sh(防止密钥落盘——PreToolUse 在写入前拦截):
#!/usr/bin/env bash
set -euo pipefail
input=$(cat)
content=$(echo "$input" | jq -r '.tool_input.content // .tool_input.new_string // ""')
# 常见密钥模式:AWS、OpenAI、私钥头
if echo "$content" | grep -qiE 'AKIA[0-9A-Z]{16}|sk-[a-zA-Z0-9]{20,}|-----BEGIN (RSA |EC )?PRIVATE KEY-----'; then
cat <<EOF
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "检测到疑似密钥/私钥,已拦截写入。请改用环境变量或密钥管理服务。"
}
}
EOF
exit 0
fi
exit 0
测试流程:在一个交互会话里,让 Claude "把 AWS 密钥 AKIA... 写进 config.py"——你会看到 deny 反馈,文件不会被创建。再让它 "执行 rm -rf node_modules /"——guard-bash 钩子拦截。而正常写代码会被 PostToolUse 自动 prettier 格式化。每条提问顶部的注入上下文让你随时知道当前分支状态。需要确认时桌面弹窗响起。
整个护栏系统的请求-拦截-反馈闭环如下:
sequenceDiagram
participant U as 用户
participant CC as Claude Code
participant H as 钩子脚本
participant T as 工具(Bash/Edit)
U->>CC: "把密钥写进 config.py"
CC->>H: PreToolUse(Edit) JSON via stdin
H->>H: 正则匹配到 AKIA 密钥
H-->>CC: exit0 + permissionDecision: deny
CC-->>CC: 拦截,将 reason 反馈给模型
CC-->>U: "已拦截:检测到密钥,建议用环境变量"
Note over U,T: 危险写入从未发生
U->>CC: "给 utils.ts 加个工具函数"
CC->>H: PreToolUse(Edit) 检查通过
H-->>CC: exit0 放行
CC->>T: 执行 Edit 写入
T-->>CC: 完成
CC->>H: PostToolUse(Write)
H->>H: prettier --write 格式化
H-->>CC: exit0
CC-->>U: 已完成并自动格式化
这就是一个"自动批准但有安全网"的工作流:权限策略可以适度放宽,因为钩子守门员会兜住真正的红线,既保持流畅又守住底线。
随着规模扩大,几个进阶点值得关注:
FileChanged、CwdChanged、InstructionsLoaded、TaskCreated、TaskCompleted 等不阻塞主循环,适合做可观测性埋点(把每次工具调用与子任务完成记入 JSONL 日志,用于事后审计与性能分析)。timeout(默认 60 秒)。超时或崩溃的钩子会被记为失败,但默认不会中断会话;对必须成功的护栏,应在脚本内做好防御性编程与超时控制。SubagentStart/SubagentStop 让你能追踪并行编排的子任务生命周期,结合日志可还原复杂的 fan-out 执行图。一个值得养成的工程习惯:用 claude --debug 启动会话调试钩子,它会把每个钩子的 stdin/stdout/exit code 完整打印出来,是排查"为什么我的钩子没生效/被忽略"的利器。
https://code.claude.com/docs/en/hooks-guide —— 从零配置到第一个可用钩子的官方教学https://code.claude.com/docs/en/hooks —— 事件清单、JSON schema、退出码语义、决策控制的权威定义https://aiorg.dev/blog/claude-code-hooks —— 20+ 可复制粘贴的实战配置(格式化、审计、安全护栏)https://dev.to/mikelane/building-guardrails-for-ai-coding-assistants-a-pretooluse-hook-system-for-claude-code-ilj —— 深入 PreToolUse 拦截系统的设计思路https://gist.github.com/FrancisBourre/50dca37124ecc43eaf08328cdcccdb34 —— 输入输出 JSON 结构的实现级速查表| 维度 | 评估 |
|---|---|
| 适合的训练营类型 | 企业级 AI 编码训练营、DevOps 自动化进阶营、安全合规导向的 AI 工程营。本主题强调"可控、可审计、防事故",契合企业落地诉求 |
| 学员基础要求 | 中级:需懂 shell 脚本、JSON、git,最好已完成 Claude Code 入门与权限配置章节 |
| 预计教学时长 | 3–4 小时(含动手实验)。建议拆为:理论 45 分钟、五个钩子逐个实现 90 分钟、完整护栏项目实战 60 分钟、调试与排错 30 分钟 |
| 实验产出 | 一套随仓库分发的 .claude/settings.json + 钩子脚本,可作为学员回到真实项目后直接复用的资产 |
| 风险提示 | 钩子是确定性拦截,但规则靠正则,存在误报/漏报。教学时应强调:护栏是"深度防御"的一层,不替代 code review 与最小权限原则;禁止依赖单一钩子保障生产安全 |
[!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 等场景全覆盖。
