Claude Code Hooks教程:从零构建5个生产级Hook
Claude Code大约95%的时间都能执行正确的操作。剩下的5%包括强制推送到main分支、跳过格式化工具以及提交未通过lint检查的代码。Hooks通过在Claude工作流的17个生命周期节点添加确定性门控来消除这5%的风险。它们每次都会触发,无一例外,不受提示措辞或模型行为的影响。
摘要: Hooks是由Claude Code生命周期事件触发的Shell命令。PreToolUse hooks用于检查并阻止操作(退出码2 = 阻止,退出码0 = 允许)。PostToolUse hooks用于事后验证和格式化。在.claude/settings.json中配置它们,使用matcher正则表达式和嵌套的hooks数组。本教程将构建五个生产级hook:自动格式化器、安全门控、测试运行器、通知提醒和提交前质量检查。
核心要点
- 独立开发者: 从自动格式化器(Hook 1)和安全门控(Hook 2)开始。这两个hooks能以零维护成本防止最常见的Claude Code错误。
- 团队负责人: 将hooks提交到仓库的
.claude/settings.json中。每位团队成员都能自动获得相同的安全门控和质量检查。 - 安全工程师: 退出码2阻止操作。退出码1仅记录警告。每个PreToolUse安全hook必须使用
exit 2,否则不会产生任何强制效果。
什么是Hooks?
Hooks是在Claude Code会话期间特定生命周期事件触发时执行的Shell命令。它们作为普通脚本在LLM外部运行,由Claude的操作触发,而非由模型解释的提示触发。
四个关键类别涵盖了最常见的使用场景(Claude Code共支持17种事件类型):
- 会话事件:
SessionStart和Stop在会话开始或结束时触发。用于初始化、清理和通知。 - 工具事件:
PreToolUse和PostToolUse在Claude使用工具(写入文件、运行bash命令或搜索代码)之前和之后触发。这些是最强大的hooks,因为它们可以检查并阻止特定操作。 - 通知事件:
Notification在Claude生成通知时触发。适用于将告警路由到Slack、桌面通知或日志系统。 - 子代理事件:
SubagentStop在子代理(通过Agent工具生成)完成时触发。Hooks也会对子代理的操作触发,因此您的安全门控会递归生效。
退出码的语义至关重要。 退出码0表示成功(继续执行)。退出码2表示阻止操作。退出码1表示非阻塞性hook错误,操作仍会继续执行。每个安全关键hook必须使用exit 2才能真正执行门控。
Hook配置基础
Hooks存储在您的配置文件中:
- 项目级别: 仓库根目录的
.claude/settings.json(与团队共享) - 用户级别:
~/.claude/settings.json(您的个人hooks,全局应用)
JSON结构:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "/path/to/your/script.sh"
}
]
}
],
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "/path/to/another-script.sh"
}
]
}
]
}
}
每个条目包含一个matcher(匹配工具名称的正则表达式,如Bash、Write、Edit、Read、Glob、Grep或Agent)和一个hook定义的hooks数组。每个hook指定一个type(Shell命令使用"command")和要运行的command。匹配器Write|Edit可同时匹配这两种工具类型。
您也可以在Claude Code会话中使用/hooks斜杠命令交互式管理hooks。
当hook触发时,Claude Code通过环境变量(文件操作使用$FILE_PATH)和标准输入(包含工具名称、参数和会话元数据的JSON对象)传递上下文。您的脚本读取这些信息来做出决策。
5个实用Hooks
以下每个hook都解决了我在将Claude Code作为主要开发工具使用时遇到的真实问题。所有示例都使用正确的嵌套hook模式。
1. 文件编辑后自动格式化
Claude编写的代码在功能上是正确的,但偶尔会违反项目的格式化规则。与其让Claude重新格式化,不如在每次文件写入后自动运行格式化工具。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "bash -c 'if [[ \"$FILE_PATH\" == *.py ]]; then black --quiet \"$FILE_PATH\" 2>/dev/null; elif [[ \"$FILE_PATH\" == *.js ]] || [[ \"$FILE_PATH\" == *.ts ]]; then npx prettier --write \"$FILE_PATH\" 2>/dev/null; fi'"
}
]
}
]
}
}
Claude Code将$FILE_PATH设置为被修改的文件。该hook检查文件扩展名并运行相应的格式化工具。Python文件使用black,JavaScript和TypeScript文件使用prettier。2>/dev/null抑制冗余输出,使您只看到实际错误。
对于较大的项目,建议将内联命令移到独立脚本中以提高可读性。
2. 危险命令的安全门控
Bash工具上的PreToolUse hooks会检查Claude即将运行的命令,如果命令匹配危险模式则将其阻止。我是在Claude在一次重构会话中强制推送到main分支后编写了这个hook。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash -c 'INPUT=$(cat); CMD=$(echo \"$INPUT\" | jq -r \".tool_input.command\"); if echo \"$CMD\" | grep -qE \"rm\\s+-rf\\s+/|git\\s+push\\s+(-f|--force)\\s+(origin\\s+)?main|git\\s+reset\\s+--hard|DROP\\s+TABLE|:(){ :|:& };:\"; then echo \"BLOCKED: Dangerous command detected: $CMD\" >&2; exit 2; fi'"
}
]
}
]
}
}
当此hook以退出码2退出时,Claude Code会取消待执行的命令。错误信息会同时输出到您的终端和Claude的上下文中,使模型理解操作失败的原因并建议更安全的替代方案。
阻止的模式:
- rm -rf /(从根目录递归删除)
- git push --force main和git push -f main(强制推送到main分支)
- git reset --hard(销毁未提交的工作)
- DROP TABLE(意外的数据库销毁)
- Fork炸弹
请根据您的环境自定义此列表。生产数据库需要添加破坏性SQL模式。基于CLI的部署需要添加部署命令防护。
3. 代码变更后运行测试
当Claude编辑Python文件时,自动运行相关测试。这能立即捕获回归问题,而不是在三个文件编辑之后才发现。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "bash -c 'if [[ \"$FILE_PATH\" == *.py ]] && [[ \"$FILE_PATH\" != *test_* ]]; then TEST_FILE=\"tests/test_$(basename \"$FILE_PATH\")\"; if [[ -f \"$TEST_FILE\" ]]; then python -m pytest \"$TEST_FILE\" -x --tb=short 2>&1 | tail -20; fi; fi'"
}
]
}
]
}
}
该hook检查编辑的文件是否为Python源文件(而非测试文件本身),使用test_前缀命名约定查找对应的测试文件,如果找到则运行它。-x标志在第一个失败时停止,tail -20保持输出简洁。
注意: 此hook假设使用带有test_前缀命名的扁平tests/目录结构。对于嵌套测试目录或不同的命名约定,请调整路径构造逻辑。
此hook在重构会话中尤其有价值,因为Claude会涉及多个文件。即时反馈循环可以防止”修复一处、破坏三处”的级联效应,这种情况在测试仅在最后运行时经常发生。
4. 会话结束时发送通知
长时间运行的Claude Code会话可能持续数分钟。与其盯着终端看,不如在会话结束时收到通知。
{
"hooks": {
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude Code session ended\" with title \"Claude Code\"'"
}
]
}
]
}
}
这个macOS示例使用osascript触发原生通知。对于Linux,将osascript行替换为notify-send "Claude Code" "Session ended"。对于Slack通知,使用webhook:
curl -s -X POST "$SLACK_WEBHOOK_URL" \
-H 'Content-type: application/json' \
-d '{"text": "Claude Code session ended"}'
我在使用& <task>(Claude Code的后台模式)启动的后台任务中使用Slack变体。桌面通知则用于交互式会话。
5. 提交前质量检查
在Claude运行git commit之前,验证代码是否通过lint检查。这能捕获仅靠格式化无法发现的问题:未使用的导入、未定义的变量、类型错误。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash -c 'INPUT=$(cat); CMD=$(echo \"$INPUT\" | jq -r \".tool_input.command\"); if echo \"$CMD\" | grep -qE \"^git\\s+commit\"; then if ! LINT_OUTPUT=$(ruff check . --select E,F,W 2>&1); then echo \"LINT FAILED -- fix before committing:\" >&2; echo \"$LINT_OUTPUT\" >&2; exit 2; fi; fi'"
}
]
}
]
}
}
此hook仅在Bash命令以git commit开头时激活。它运行ruff(一个快速的Python linter),启用错误、pyflakes和警告规则。如果存在任何问题,提交将被阻止(exit 2),Claude会看到lint输出,这通常会促使它修复问题并重试。
您可以叠加多个质量检查:用mypy做类型检查,用bandit做安全扫描,或使用项目的自定义验证脚本。Bash命令上的PreToolUse hooks为您提供了在任何Shell操作之前的可编程门控。
Hook调试技巧
Hooks静默失败的频率超出您的预期。以下是我用于调试的五个技巧:
- 先独立测试脚本。 手动将示例JSON通过管道传入脚本:
echo '{"tool_input":{"command":"git commit -m test"}}' | bash your-hook.sh。如果在Claude Code外部失败,在内部也会失败。 - 使用stderr输出调试信息。 hook写入stderr的任何内容都会出现在Claude的上下文中。开发时写入
echo "DEBUG: matched $CMD" >&2,hook稳定后再删除。 - 注意jq失败。 如果您的JSON路径错误,
jq会静默返回null,您的条件判断将不会匹配。请针对真实的工具输入测试您的jq表达式。 - 验证退出码。 退出码2阻止操作。退出码1仅发出警告。一个意外使用
exit 1的PreToolUse hook在看似正常工作的同时提供零强制力。默认使用宽容模式(exit 0),仅对特定阻止模式使用exit 2。 - 保持hooks快速。 Hooks同步运行。一个耗时5秒的hook会给每次匹配的工具使用增加5秒延迟。我将所有hooks控制在2秒以内,理想情况下在500毫秒以内。
后续步骤
这五个hooks涵盖了基础功能:格式化、安全、测试、通知和质量门控。当您熟悉这些模式后,可以构建用于上下文注入(在会话启动时添加项目特定指令)、递归防护(防止无限子代理循环)和工作流编排(串联多步骤流程)的hooks。
关于hook架构、全部17个生命周期事件和高级模式,请参阅我完整指南中的hooks章节:Claude Code指南:Hooks如何工作?
我还在Claude Code Hooks:我的95个生产级Hooks背后的故事中记录了每个hook的起源故事,涵盖了促使我创建它们的各种事件。
常见问题
Hooks能阻止Claude Code运行命令吗?
可以。PreToolUse hooks通过以退出码2退出来阻止任何工具操作。Claude Code取消待执行的操作并将hook的stderr输出展示给模型。退出码1是非阻塞性hook错误,操作仍会继续执行。这一区别至关重要:每个安全hook必须使用exit 2,而非exit 1。Claude会看到拒绝原因并建议更安全的替代方案。
Hook配置文件放在哪里?
Hook配置放在.claude/settings.json中用于项目级hooks(提交到仓库,与团队共享),或放在~/.claude/settings.json中用于用户级hooks(个人使用,应用于所有项目)。当两者同时存在时,项目级hooks优先。建议对脚本文件使用绝对路径,以避免工作目录问题。
Hooks对子代理有效吗?
有效。Hooks也会对子代理的操作触发。如果Claude通过Agent工具生成子代理,您的PreToolUse和PostToolUse hooks会对子代理使用的每个工具执行。如果没有这一行为,子代理就可能绕过您的安全门控。SubagentStop事件允许您在子代理完成任务时运行清理或验证操作。