04 - Hooks 体系设计
1. 定位
Hooks 是确定性的质量闸门。与 CLAUDE.md 的"建议性"指令不同,Hooks 保证每次条件满足时执行。
与 CLAUDE.md 的本质区别(官方明确说明):
- CLAUDE.md 指令是建议性的 — Claude 尽力遵循但不保证每次执行
- Hooks 是确定性的 — 条件满足即保证执行,无例外
核心价值:
- 把"必须每次无例外执行"的动作从"靠 AI 记住"变为"自动执行"
- 减轻 CLAUDE.md 的负担(不需要写"每次编辑后运行 prettier"这类规则)
- 经验法则:如果某条规则违反一次就会造成问题,用 Hook 而非 CLAUDE.md
2. Hook 事件:按防线目的选择
不要从事件列表出发想"我能用哪些 Hook",而是从防线需求出发想"我需要防什么"。
安全防线 — 阻止不可逆操作
PreToolUse(Bash)— 拦截危险命令(rm -rf、force push、sudo)PreToolUse(Edit|Write)— 拦截对关键文件的修改PermissionRequest— 自动审批策略、审计记录
质量防线 — 保证产出标准
PostToolUse(Write|Edit)— 编辑后自动格式化、文件行数检查PostToolUseFailure— 工具失败后的自动诊断或重试策略TaskCompleted— Agent Teams 任务完成时的质量门控
上下文防线 — 防止信息丢失
PreCompact— 压缩前注入保留指令,保护关键上下文InstructionsLoaded— 审计 CLAUDE.md/rules 的加载情况
生命周期 — 环境管理
SessionStart/SessionEnd— 会话启动/结束时的环境加载和资源清理SubagentStart/SubagentStop— 子代理启动前注入指令、完成后聚合结果TeammateIdle— Agent Teams 成员空闲时重新分配任务- 其他:
UserPromptSubmit、Stop、Notification、Prompt、ConfigChange、WorktreeCreate/WorktreeRemove
个人开发者的核心三件套:PreToolUse(安全)、PostToolUse(质量)、PreCompact(上下文保护)。其他按需启用。
完整事件列表、Matcher 语法和 JSON 输入输出格式参见 Hooks 官方参考。本文档聚焦选择判断和实际配置。
3. Hook 决策机制
Hook 通过两种方式告诉 Claude Code 该怎么做:
简单模式:退出码
| 退出码 | 含义 | 效果 |
|---|---|---|
| 0 | 通过 | 工具调用正常执行 |
| 1 | 警告 | 不阻断,但 AI 会看到 stderr 中的警告信息 |
| 2 | 阻断 | 阻止操作,stderr 内容反馈给 AI 指导修正 |
结构化模式:JSON 输出(推荐)
对于 PreToolUse 等决策类 Hook,推荐输出结构化 JSON,语义更清晰:
# stdout 输出 JSON 决策
jq -n '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: "Destructive command blocked by hook"
}
}'两种模式等效,新写的 Hook 推荐用 JSON 模式。关键点:无论哪种模式,阻断时的反馈信息不只是"拦截",更是"指导"——AI 会据此修正行为。
4. Hook 输入数据
Hook 通过 stdin 接收 JSON 格式的上下文数据:
// PreToolUse / PostToolUse
{
"tool_name": "Bash",
"tool_input": {
"command": "rm -rf /tmp/test"
}
}
// PostToolUse 额外包含
{
"tool_name": "Edit",
"tool_input": {
"file_path": "/src/app.ts",
"old_string": "...",
"new_string": "..."
},
"tool_output": "File edited successfully"
}5. 生产级 Hook 配置
5.1 完整的 settings.json hooks 配置
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{
"type": "command",
"command": ".claude/hooks/pre-bash-firewall.sh"
}]
}
],
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [{
"type": "command",
"command": ".claude/hooks/post-edit-format.sh"
}]
}
],
"PreCompact": [
{
"matcher": "auto",
"hooks": [{
"type": "command",
"command": ".claude/hooks/pre-compact-preserve.sh"
}]
}
]
}
}5.2 安全防火墙 — pre-bash-firewall.sh
阻断危险的 Bash 命令(使用 JSON 结构化输出):
#!/usr/bin/env bash
set -euo pipefail
input=$(cat)
cmd=$(echo "$input" | jq -r '.tool_input.command // ""')
dangerous_patterns=(
'rm -rf /'
'git reset --hard'
'git push.*--force'
'git clean -f'
'sudo '
'chmod 777'
'curl.*|.*sh'
'wget.*|.*sh'
)
for pattern in "${dangerous_patterns[@]}"; do
if echo "$cmd" | grep -qE "$pattern"; then
jq -n --arg reason "Blocked dangerous pattern '$pattern' in: $cmd" '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: $reason
}
}'
exit 0
fi
done
exit 05.3 编辑后自动格式化 — post-edit-format.sh
#!/usr/bin/env bash
set -euo pipefail
input=$(cat)
file_path=$(echo "$input" | jq -r '.tool_input.file_path // ""')
# 只处理项目内的文件
case "$file_path" in
*.ts|*.tsx|*.js|*.jsx)
if command -v npx &> /dev/null; then
npx prettier --write "$file_path" 2>/dev/null || true
fi
;;
*.json)
if command -v npx &> /dev/null; then
npx prettier --write "$file_path" 2>/dev/null || true
fi
;;
esac
exit 05.4 自动压缩时保留上下文 — pre-compact-preserve.sh
#!/usr/bin/env bash
cat <<'EOF'
{
"hookSpecificOutput": {
"hookEventName": "PreCompact",
"additionalContext": "压缩时必须保留:1. 所有已修改文件的路径 2. 当前任务进度和下一步计划 3. 测试命令和运行结果 4. 重要的架构决策 5. 失败的尝试及原因"
}
}
EOF
exit 06. 运行时规范强制执行 Hooks
除了前面 3 个核心 Hook,以下 Hook 在运行时自动强制执行编码规范,不依赖 AI "记住"规则:
6.1 写入后文件行数检查 — post-write-size-guard.sh
AI 写入或编辑文件后,自动检查是否超过 300 行限制:
#!/usr/bin/env bash
set -euo pipefail
input=$(cat)
file_path=$(echo "$input" | jq -r '.tool_input.file_path // ""')
# 只检查 TS/TSX 文件
case "$file_path" in
*.ts|*.tsx)
if [ -f "$file_path" ]; then
lines=$(wc -l < "$file_path")
if [ "$lines" -gt 300 ]; then
echo "WARNING: $file_path 已达 ${lines} 行,超过 300 行限制。请考虑拆分。" >&2
# 退出码 1 = 非阻断警告,AI 会看到但不会被拦截
exit 1
fi
fi
;;
esac
exit 0注意:退出码是 1 不是 2。不阻断写入,但 AI 会收到警告并据此行动。
6.2 新依赖拦截 — pre-bash-dep-guard.sh
AI 执行 npm install 时,检查是否在白名单中:
#!/usr/bin/env bash
set -euo pipefail
input=$(cat)
cmd=$(echo "$input" | jq -r '.tool_input.command // ""')
# 只拦截 npm install / npm add / yarn add / pnpm add
if ! echo "$cmd" | grep -qE '(npm install|npm add|yarn add|pnpm add)'; then
exit 0
fi
# 提取包名(去掉 flags 和版本号)
pkg=$(echo "$cmd" | grep -oE '(npm install|npm add|yarn add|pnpm add)\s+\S+' | awk '{print $NF}' | sed 's/@.*//')
if [ -z "$pkg" ] || [ "$pkg" = "-D" ] || [ "$pkg" = "--save-dev" ]; then
exit 0 # 无包名或只有 flag,放行
fi
# 检查禁止列表
banned=("moment" "jquery" "lodash")
for b in "${banned[@]}"; do
if [ "$pkg" = "$b" ]; then
echo "BLOCKED: $pkg 在禁止依赖列表中。请查看 docs/approved-deps.md 获取替代方案。" >&2
exit 2
fi
done
# 检查白名单(如果存在)
if [ -f "docs/approved-deps.md" ]; then
if ! grep -q "$pkg" docs/approved-deps.md; then
echo "WARNING: $pkg 不在依赖白名单中。请评估是否需要,通过后添加到 docs/approved-deps.md。" >&2
exit 1 # 警告但不阻断
fi
fi
exit 06.3 settings.json 中的完整 Hooks 配置
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{"type": "command", "command": ".claude/hooks/pre-bash-firewall.sh"},
{"type": "command", "command": ".claude/hooks/pre-bash-dep-guard.sh"}
]
}
],
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{"type": "command", "command": ".claude/hooks/post-edit-format.sh"},
{"type": "command", "command": ".claude/hooks/post-write-size-guard.sh"}
]
}
],
"PreCompact": [
{
"matcher": "auto",
"hooks": [
{"type": "command", "command": ".claude/hooks/pre-compact-preserve.sh"}
]
}
]
}
}6.4 三层保障总览
脚本 check Hooks Skills /check
触发方式 人工/CI 运行 每次工具调用自动 人工调用
检查时机 事后审计 实时拦截/警告 按需深度检查
能做的 文件存在性 文件行数 代码语义分析
配置格式 危险命令 CLAUDE.md 健康度
行数统计 禁止依赖 测试覆盖评估
禁止依赖 自动格式化 架构一致性
.env 安全 依赖合理性
需要 AI 否 否 是
执行速度 秒级 毫秒级 分钟级
阻断能力 CI 失败 退出码 2 阻断 建议性三者互补,不重叠:
- Hooks 管实时 — 每次编辑/命令都自动检查,零成本
- 脚本 check 管审计 — 定期或 CI 中全面扫描,不需要 AI
- Skill /check 管深度 — 需要 AI 理解代码语义时使用
7. 设计原则
7.1 三个必须
- 快速 — 每个 Hook 执行控制在 2 秒内。格式化单文件通常 < 500ms。如果 Hook 太慢会严重影响开发体验
- 幂等 — 重复执行不产生副作用。
prettier --write是幂等的(格式化后再格式化结果不变) - 容错 — 非关键 Hook 失败不应阻断工作流。用
|| true或非 2 的退出码
7.2 不该用 Hook 做的事
- 复杂的逻辑判断(交给 Skill 或 CLAUDE.md 规则)
- 耗时的操作(完整测试套件不应放在 PostToolUse,应该在提交前手动运行)
- 需要 AI 推理的判断(Hook 是确定性脚本,不调用 LLM)
7.3 调试 Hook
Hook 出问题时的排查方式:
# 手动测试 Hook,模拟 stdin 输入
echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /"}}' | .claude/hooks/pre-bash-firewall.sh
# 应输出包含 permissionDecision: "deny" 的 JSON
echo '{"tool_name":"Bash","tool_input":{"command":"npm test"}}' | .claude/hooks/pre-bash-firewall.sh
echo $? # 应该输出 0(放行)7. 失败恢复
四份参考文件都没有覆盖的盲区——Hook 失败时怎么办:
| 场景 | 处理策略 |
|---|---|
| 格式化 Hook 失败(prettier 未安装) | ` |
| 安全 Hook 误判(拦截了正常命令) | 修改 Hook 的正则模式,放宽匹配 |
| Hook 执行超时 | 检查 Hook 脚本,优化或移除耗时操作 |
| 所有 Hook 都不工作 | 检查 settings.json 的 hooks 配置格式和文件路径 |
8. 与其他子系统的关系
CLAUDE.md:减轻负担
"不需要写格式化规则" → Hook 自动处理
Skills:互补
Skill 负责流程编排 → Hook 负责自动质量关卡
SubAgents:同样生效
子代理的工具调用同样受 PreToolUse Hook 拦截
settings.json permissions:协同
permissions 控制"允许/禁止" → Hook 控制"自动处理"9. Hook 类型扩展
除标准 Shell Command Hooks 外,Claude Code 还支持以下高级 Hook 类型。
9.1 HTTP Hooks
通过 type: http 将事件 POST 到外部端点,替代本地脚本。支持自定义 headers 和环境变量插值:
// settings.json
{
"hooks": {
"NotificationSend": [{
"type": "http",
"url": "https://your-server.com/webhook",
"headers": {
"Authorization": "Bearer ${API_TOKEN}", // env var interpolation
"X-Project": "my-project"
},
"transport": "sse" // 支持 sse | fetch
}]
}
}headers中的${VAR_NAME}会自动替换为同名环境变量- 适用场景:集中日志收集、团队协作通知、远程审计、CI/CD 集成
9.2 Prompt Hooks
Prompt 事件在 API 请求发送前触发,允许修改发送给模型的完整 prompt:
- 注入额外上下文(如从外部知识库检索的 RAG 片段)
- 审计/记录所有发送的 prompt 内容
- 对敏感信息进行脱敏处理
{
"hooks": {
"Prompt": [{
"type": "command",
"command": "python3 scripts/prompt-filter.py"
}]
}
}9.3 SubagentStart 事件
子代理启动前触发,可用于:
- 为特定类型的子代理注入定制指令
- 基于策略阻止不必要的子代理启动
- 记录子代理的启动参数用于调试
9.4 Skill 范围内的 Hooks
Skills 和 Agents 可通过 frontmatter 的 hooks 字段声明专属 Hook,仅在该 Skill 激活期间生效,Skill 结束后自动移除:
---
name: review
hooks:
PostToolUse:
- command: ".claude/hooks/strict-lint-check.sh"
event_filter: "Write|Edit"
PreToolUse:
- command: ".claude/hooks/block-force-push.sh"
event_filter: Bash
---
# 以下是 Skill 正文...典型用例:
/reviewSkill 激活时启用更严格的 Lint 检查/deploySkill 激活时额外验证部署环境变量- 避免全局 Hook 配置过重,按需加载