与其让模型记住你的项目规则,不如把规则写进 Hook 脚本——Hook 是 Claude Code 最被低估的能力,它让你从"祈祷模型听话"升级为"用代码强制执行策略"。
▲ ▲ 图1:Claude Code Hook 事件系统架构 — 27个事件、stdin/stdout JSON协议、5大核心Hook的决策流程
为什么你需要关心 Hook
用 Claude Code(或任何 AI Coding Agent)写过代码的人都遇到过类似困境:
- 你告诉它"不要提交密钥到 Git",但它偶尔还是会漏掉
- 你让它"改完代码跑测试",但它有时会跳过这一步
- 你写了长长的 CLAUDE.md 项目规则,但长上下文下模型还是会忘
根本原因:把规则放在模型的记忆里,和把规则放在模型外面,是完全不同的两件事。 记忆会丢失、会忽略、会被后面的上下文覆盖。而 Hook 是模型每次调用工具时都要经过的一道门——不管你上下文有多长,不管你之前说了什么,Hook 脚本都会执行。
Claude Code 提供了 27 个 Hook 事件。本文基于对全部 Hook 的逐一研究,提炼出最值得投入的 5 个核心 Hook 和几个场景化 Hook,附带可直接使用的代码示例和踩坑经验。
Hook 的工作原理
在 .claude/settings.json 中配置 Hook:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "/home/user/.claude/hooks/pre-bash.sh",
"timeout": 30
}
]
}
]
}
}
当事件触发时,Claude Code 执行你的命令,通过 stdin 传入 JSON,从 stdout 读取 JSON 决定下一步行为:
// stdin 示例(PreToolUse 事件)
{
"event": "PreToolUse",
"session_id": "abc123",
"transcript_path": "/tmp/claude-transcript.json",
"cwd": "/home/user/project",
"tool_name": "Bash",
"tool_input": { "command": "git push origin main" }
}
你的脚本通过 stdout 返回的控制选项:
continue: false — 直接中止本次调用decision: "block" — 阻止工具执行,给出原因hookSpecificOutput.additionalContext — 注入文本到下一个模型推理轮次hookSpecificOutput.updatedInput — 改写工具的输入参数(PreToolUse 专用)- exit 0 + 空 stdout — 无操作,放行
非零退出码会作为 Hook 错误记录在对话记录中。
5 个核心 Hook,每个都有明确的适用场景
1. PreToolUse — 最强大的 Hook
触发时机:每次工具调用之前。
这是最重要的 Hook,因为它不仅能观察,还能改写输入(updatedInput)。这意味着你可以在命令发给 Shell 执行之前就拦截并修改它。
实战场景一:防止密钥泄露
#!/bin/bash
# pre-bash.sh — 扫描命令中的密钥并拦截
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
# 检测常见的密钥模式
if echo "$COMMAND" | grep -qE '(sk-[a-zA-Z0-9]{20,})|(ghp_[a-zA-Z0-9]{36})|(-----BEGIN.*PRIVATE KEY-----)'; then
echo '{"continue": true, "decision": "block", "reason": "命令中包含疑似密钥/Token,已拦截。请使用环境变量替代。"}'
exit 0
fi
# 自动改写:将硬编码的 API key 替换为环境变量引用
SAFE_CMD=$(echo "$COMMAND" | sed 's/sk-[a-zA-Z0-9]\{20,\}/$OPENAI_API_KEY/g')
if [ "$COMMAND" != "$SAFE_CMD" ]; then
echo "{\"continue\": true, \"hookSpecificOutput\": {\"updatedInput\": {\"command\": $(echo "$SAFE_CMD" | jq -Rs .)}}}"
exit 0
fi
echo '{}' # 放行
实战场景二:禁止在主分支直接推送
#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
if [ "$BRANCH" = "main" ] && echo "$COMMAND" | grep -q "git push"; then
echo '{"continue": true, "decision": "block", "reason": "禁止直接推送到 main 分支。请创建 feature 分支后提 PR。"}'
exit 0
fi
echo '{}'
实战场景三:禁止危险数据库操作
#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
UPPER=$(echo "$COMMAND" | tr '[:lower:]' '[:upper:]')
if echo "$UPPER" | grep -qE '\b(DROP TABLE|DELETE FROM|TRUNCATE)\b'; then
echo '{"continue": true, "decision": "block", "reason": "危险 SQL 操作已拦截。如需执行,请手动在终端操作并确认。"}'
exit 0
fi
echo '{}'
⚠️ 踩坑提醒:PreToolUse 的 updatedInput 要求返回完整的工具输入对象,不是只返回被修改的字段。上面的 jq -Rs . 确保命令字符串被正确 JSON 编码。如果返回格式错误,Hook 会静默失败,工具以原始输入执行——不会报错,但也不会生效。
2. UserPromptSubmit — 动态版 CLAUDE.md
触发时机:你每次按 Enter 发送消息时。
它的返回值 additionalContext 会作为系统消息注入到下一个模型推理轮次。这相当于一个按上下文动态变化的 CLAUDE.md——比写死的项目规则文件更灵活。
#!/bin/bash
# user-prompt-submit.sh — 根据用户输入动态注入上下文
INPUT=$(cat)
PROMPT=$(echo "$INPUT" | jq -r '.prompt // ""')
ADDITIONS=""
# 场景1:用户提到 PII 或用户数据时注入合规提醒
if echo "$PROMPT" | grep -qiE '(用户数据|PII|个人信息|隐私)'; then
ADDITIONS="$ADDITIONS
⚠️ 合规提醒:
- 不得在日志中输出用户真实姓名、手机号、身份证号
- 测试数据必须使用假名和脱敏数据
- 如需处理真实用户数据,请先确认已获得授权"
fi
# 场景2:自动关联相关文件
RELATED=$(grep -l "$(echo "$PROMPT" | head -c 50)" src/**/*.ts 2>/dev/null | head -5 | tr '\n' ',')
if [ -n "$RELATED" ]; then
ADDITIONS="$ADDITIONS
📁 可能相关的文件: $RELATED"
fi
# 场景3:API 花费阈值检查(示例:从本地监控服务获取当日花费)
SPEND_TODAY=$(cat /tmp/daily-api-spend 2>/dev/null || echo "0")
if [ "$SPEND_TODAY" -gt 50 ]; then
ADDITIONS="$ADDITIONS
💰 今日 API 花费已达 $${SPEND_TODAY},请优先使用缓存结果,减少重复调用。"
fi
if [ -n "$ADDITIONS" ]; then
echo "{\"continue\": true, \"hookSpecificOutput\": {\"additionalContext\": $(echo "$ADDITIONS" | jq -Rs .)}}"
else
echo '{}'
fi
▲ ▲ 图2:PreToolUse 安全防护三场景 — 密钥检测替换、主分支推送保护、危险操作拦截
3. PostToolUse — 强制测试执行
触发时机:每次工具调用成功之后。
这个 Hook 的价值在于消除"模型改完代码忘记跑测试"这类重复性错误。
#!/bin/bash
# post-tool-use.sh — Edit 操作后自动提醒跑测试
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool_name // ""')
if [ "$TOOL" = "Edit" ] || [ "$TOOL" = "Write" ]; then
echo '{"continue": true, "hookSpecificOutput": {"additionalContext": "📋 检测到文件修改操作。请确认是否需要运行相关测试:npm test / cargo test / pytest。"}}'
else
echo '{}'
fi
进阶用法——从 Bash 输出中脱敏:
#!/bin/bash
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool_name // ""')
OUTPUT=$(echo "$INPUT" | jq -r '.tool_output // ""')
if [ "$TOOL" = "Bash" ]; then
# 在模型看到输出之前,把 access token 替换为占位符
SANITIZED=$(echo "$OUTPUT" | sed 's/sk-[a-zA-Z0-9]\{20,\}/[REDACTED]/g')
if [ "$OUTPUT" != "$SANITIZED" ]; then
echo "{\"continue\": true, \"hookSpecificOutput\": {\"updatedMCPToolOutput\": $(echo "$SANITIZED" | jq -Rs .)}}"
exit 0
fi
fi
echo '{}'
⚠️ 重要:PostToolUse 不能阻止已经发生的事情——它只能影响下一个推理轮次。如果你想在工具执行之前就拦截,必须用 PreToolUse。
4. SessionStart — 会话级别的上下文注入
触发时机:会话启动、恢复、清除、compact 之后。
source 字段告诉你启动类型(startup / resume / clear / compact),可以针对不同场景注入不同上下文。最被低估的功能是 watchPaths——让你注册 cwd 之外的路径来触发 FileChanged 事件。
#!/bin/bash
INPUT=$(cat)
SOURCE=$(echo "$INPUT" | jq -r '.source // "startup"')
case "$SOURCE" in
"resume")
# 恢复会话时,注入上次关闭前的关键上下文
echo '{"continue": true, "hookSpecificOutput": {"additionalContext": "欢迎回来!上次你在开发用户认证模块,当前分支: '"$(git branch --show-current)"'。未完成的 TODO 见 docs/TODO.md"}}'
;;
"compact")
# compact 之后,保留最重要的项目规则
echo '{"continue": true, "hookSpecificOutput": {"additionalContext": "📌 核心规则(compact 后保留):\n1. TypeScript strict mode,禁止 any\n2. API 端点用 Zod 校验\n3. 每次 Edit 后运行测试"}}'
;;
*)
echo '{}'
;;
esac
5. PermissionRequest — 分类授权策略
触发时机:工具调用需要向你请求权限时。
与其每次都点"批准"或直接开 YOLO 模式,不如用代码编码授权策略:
#!/bin/bash
# permission-request.sh — 分类授权
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool_name // ""')
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
# 安全命令:自动允许
if echo "$COMMAND" | grep -qE '^(ls|cat|grep|git status|git diff|git log|find|wc|head|tail|which|pwd|echo|node -v|npm -v|python --version)'; then
echo '{"continue": true, "decision": "allow"}'
exit 0
fi
# 写操作:检查是否在当前仓库内
REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
if [ -n "$REPO_ROOT" ]; then
CWD=$(pwd)
if [[ "$CWD" != "$REPO_ROOT"* ]]; then
# 写操作发生在仓库外 → 拒绝
echo '{"continue": true, "decision": "deny", "reason": "写操作超出仓库范围。如果确实需要,请手动执行。"}'
exit 0
fi
fi
# 其他情况 → 交给用户决定
echo '{}'
💡 经验之谈:自动允许安全的只读命令(ls、cat、grep、git status 等),模型就不会被这些无风险的权限弹窗打断思路。但同时要在工作仓库外严格限制写操作。
场景化 Hook:需要时再开启
这些 Hook 不是日常必开,但在特定场景下能发挥巨大价值:
| Hook | 最佳场景 | 怎么用 |
|---|
| PostToolUseFailure | 构建/测试失败时自动诊断 | 注入 "Bash 失败原因:端口 3000 被占用,进程 PID 12345" 给模型 |
| PermissionDenied | 权限被拒后告诉模型为什么 | 返回 additionalContext 说明拒绝原因,或设置 retry: true 让模型换一种方式 |
| Stop | 防止模型草率收工 | 注入检查清单 "你真的完成了吗?请确认:测试通过?文档更新?没有 TODO?" |
| PreCompact / PostCompact | 长对话质量保持 | 在 compact 前后锚定关键事实,防止上下文压缩丢失重要信息 |
| SubagentStop | 子 Agent 结果聚合 | 将子 Agent 的碎片化结果结构化为父对话可用的格式 |
| InstructionsLoaded | 调试 "这条规则哪来的?" | load_reason 字段告诉你规则来源(session-start、nested 等) |
常见踩坑与排障
▲ ▲ 图3:PermissionRequest 分类授权策略 — 安全命令自动允许、危险命令检查仓库范围后决策
坑1:Hook 脚本退出时没有返回有效 JSON
症状:Hook 配置正确但似乎不生效,Claude Code 没有任何报错。
根因:Claude Code 对非零退出码的处理是"记录但不阻塞"——如果你的脚本因语法错误退出,它不会中断工具调用,而是静默跳过。
排查:先手动测试 Hook 脚本:
echo '{"tool_name":"Bash","tool_input":{"command":"echo test"}}' | bash /path/to/hook.sh
# 确认返回的是有效 JSON
坑2:PreToolUse 的 updatedInput 格式错误
症状:PreToolUse 返回了 updatedInput,但工具仍然以原始参数执行。
根因:updatedInput 必须是完整的工具输入对象,不能只返回修改的字段。例如对 Bash 工具,必须返回 {"command": "修改后的命令"}。
正确示例:
# ✅ 正确:返回完整的 tool_input 对象
echo '{"continue": true, "hookSpecificOutput": {"updatedInput": {"command": "safe-command"}}}'
# ❌ 错误:只返回修改的部分
echo '{"continue": true, "hookSpecificOutput": {"updatedInput": "safe-command"}}'
坑3:Hook 超时设置太短
症状:Hook 在复杂检查时被截断。
建议:涉及网络请求(如 API 花费查询)的 Hook,将 timeout 设置为 10-15 秒。本地文件扫描 2-3 秒足够。
坑4:Hook 脚本路径问题
Claude Code 从自己的 cwd 执行 Hook,不一定是你的项目根目录。始终使用绝对路径或用 $HOME 前缀。
// ❌ 错误
"command": ".claude/hooks/pre-bash.sh"
// ✅ 正确
"command": "/home/user/.claude/hooks/pre-bash.sh"
总结
Hook 体系的核心价值不在任何单个 Hook 上,而在于它重新定义了人与 AI Coding Agent 的关系:
- 从"告诉模型遵守规则" → "用代码强制规则"
- 从"手动批准每次调用" → "编码策略自动决策"
- 从"CLAUDE.md 写死的规则" → "运行时动态注入上下文"
投入 1 小时配置好 PreToolUse + PermissionRequest 两个 Hook,就能解决 80% 的重复性摩擦。剩下的 Hook 按需开启,不需要一次性全部配完。
行动建议:
- 今天:写一个 PreToolUse Hook,拦截
git push main 和密钥泄露 - 本周:配置 PermissionRequest,自动允许
ls/cat/grep/git status - 本月:按场景添加 PostToolUse(强制测试)和 SessionStart(compact 保真)
本文由AI辅助创作,经人工审核编辑发布