大家好,我是伦哥。
上一章我们聊了 Hooks 的基础概念——中间件本质、事件体系、配置结构,以及最常用的 PreToolUse 和 PostToolUse 两大实战场景。PreToolUse 在工具执行前做“入口安检”,PostToolUse 在工具执行后做“过程质检”。
但还有一个关键环节我们没有覆盖:Claude 做完整个任务后,谁来验收?
这就好比工厂的流水线:安检门(PreToolUse)检查原料是否合格,质检站(PostToolUse)检查每道工序的产出。但产品最终出厂前,还需要一道终检——确认成品整体质量达标。这道终检,就是今天要讲的重头戏:Stop Hook。
一、Stop Hook——任务完成时的“出厂验收”
Stop Hook 在 Claude 完成响应后运行。如果说 PreToolUse 是入口安检,PostToolUse 是过程质检,那么 Stop Hook 就是出厂验收——在 Claude 宣布“我做完了”之后,再检查一遍交付物的质量。
Stop Hook 和其他 Hook 最大的区别在于它的 continue: true 字段:
{
"decision": "block",
"reason": "Tests are failing, please fix them",
"continue": true
}
continue: true 意味着“不要停,继续工作”。这创造了一个自动循环:Claude 认为完成了 → Stop Hook 检查 → 发现测试失败 → 把失败信息反馈给 Claude → Claude 继续修复 → 再次完成 → 再次检查……直到所有检查通过,Claude 才被允许真正停下来。
这种机制把质量保证从“事后检查”变成了“交付前置条件”——不是做完了再检查,而是检查通过了才算做完。
实战:自动测试门控
这是 Stop Hook 最经典的应用场景。为什么要在 Stop 时运行测试,而不是在每次文件修改后?因为一个功能的实现通常涉及多个文件的修改。中间状态的测试必然会失败——你改了接口但还没改实现,测试当然过不了。只有在 Claude 认为“全部完成”的时刻,再运行测试才有意义。
来看一个完整的 Shell 脚本实现(run-tests.sh):
#!/bin/bash
set -e
# 确定项目目录
if [ -n "$CLAUDE_PROJECT_DIR" ]; then
cd"$CLAUDE_PROJECT_DIR"
fi
RUN_TESTS=false
TEST_RESULT=""
TEST_PASSED=true
# Node.js 项目
if [ -f "package.json" ]; then
RUN_TESTS=true
if grep -q '"test"' package.json; then
TEST_RESULT=$(npm test 2>&1) || TEST_PASSED=false
else
TEST_RESULT="No test script found"
TEST_PASSED=true
fi
# Python 项目
elif [ -f "pytest.ini" ] || [ -f "pyproject.toml" ]; then
RUN_TESTS=true
ifcommand -v pytest &>/dev/null; then
TEST_RESULT=$(pytest 2>&1) || TEST_PASSED=false
fi
# Go 项目
elif [ -f "go.mod" ]; then
RUN_TESTS=true
TEST_RESULT=$(go test ./... 2>&1) || TEST_PASSED=false
# Rust 项目
elif [ -f "Cargo.toml" ]; then
RUN_TESTS=true
TEST_RESULT=$(cargo test 2>&1) || TEST_PASSED=false
fi
# 如果没有检测到测试框架,直接放行
if [ "$RUN_TESTS" = false ]; then
echo'{}'
exit 0
fi
# 转义 JSON 特殊字符
TEST_RESULT_ESCAPED=$(echo"$TEST_RESULT" | head -50 | jq -Rs '.')
if [ "$TEST_PASSED" = true ]; then
echo'{"decision": "approve", "reason": "All tests passed."}'
else
cat <<EOF
{
"decision": "block",
"reason": "Tests are failing. Please fix the issues before stopping.",
"continue": true,
"systemMessage": $TEST_RESULT_ESCAPED
}
EOF
fi
exit 0
这个脚本有几个巧妙的设计:
项目类型自动检测:通过检查 package.json、pyproject.toml、go.mod、Cargo.toml 等特征文件来判断项目类型,实现了“约定优于配置”的通用性。
容错处理:先检查是否有 test 脚本。如果项目根本没配置测试,不会报错而是放行——不能因为项目还没写测试就阻止 Claude 完成工作。
结果截断:head -50 只取前 50 行。测试失败的输出可能非常长,但 Claude 只需要看到关键错误信息就能定位问题。这体现了第 6 讲中“信噪比”的思维。
配置方式:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "./hooks/run-tests.sh"
}
]
}
]
}
}
注意 Stop 事件没有 matcher 字段——因为 Stop 是生命周期事件,不针对特定工具。
当 Claude 写完代码准备停下来时,如果测试失败,你会看到:
Stop hook returned blocking error
Tests are failing. Please fix the issues before stopping.
[测试失败的详细输出...]
Claude 继续修复代码...
即使 Claude 拼命说任务完成了,Stop Hook 都会让它继续改错。这个机制在 Claude 执行复杂重构时特别有价值——重构常常会破坏现有测试,而这个 Hook 确保 Claude 会把测试修好才停止。
二、Prompt 类型 Hook:让 AI 当代码审查员
Shell 脚本适合检查客观事实——测试通不过、文件不存在。但有时候你需要检查更“主观”的东西:代码风格是否合理?功能实现是否完整?有没有遗漏边界情况?这些判断需要“理解力”,不是模式匹配能解决的。
这时可以用 Prompt 类型的 Stop Hook,让一个小型 LLM 担任代码审查员:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "Review the changes made in this session. Check that: 1) Code follows best practices 2) No obvious bugs 3) Error handling is adequate"
}
]
}
]
}
}
这相当于在 Claude 完成工作后,让另一个 AI 做 Code Review。主 Claude 是作者,Prompt Hook 的模型是审查者。审查者往往能发现作者忽略的问题,因为它没有“我刚写的代码当然是对的”这种认知偏见。
当然,Prompt 类型的可靠性低于 Command 类型。LLM 可能漏检,也可能误报。但作为测试门控之外的第二层防线,它能覆盖一些脚本无法检查的维度。
三、防止死循环:stop_hook_active 安全开关
Stop Hook 的 continue: true 很强大,但也有风险——如果 Claude 一直修不好,就会进入死循环。
官方提供了安全字段 **stop_hook_active**:当 Claude 因为 Stop Hook 而继续工作时,下一次 Stop 事件的输入中 stop_hook_active 会被设为 true。
#!/bin/bash
INPUT=$(cat)
# 检查是否已经因为 Stop Hook 继续过了
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
# 已经重试过一次了,这次让 Claude 停下来
exit 0
fi
# 正常的测试逻辑
npm test 2>&1
if [ $? -ne 0 ]; then
echo'{"decision": "block", "reason": "Tests still failing, please fix.", "continue": true}'
else
exit 0
fi
这个模式允许 Claude 重试一次——第一次 Stop 时检查测试,如果失败就让 Claude 继续修复;第二次 Stop 时,stop_hook_active 为 true,无论测试是否通过都让 Claude 停下来。
四、SubagentStart 与 SubagentStop——子代理的入口与出口
在第 3-8 讲中,我们学习了子代理的各种使用模式。现在从 Hook 的角度来看——如何在子代理启动和完成时自动执行检查?
SubagentStart:为子代理注入上下文
SubagentStart 在子代理被启动时触发。它的 matcher 匹配的是子代理类型名,而不是工具名。
.claude/agents/ 中定义的子代理 |
SubagentStart 不能阻止子代理启动(这是设计决策——启动子代理是主会话的明确意图),但可以通过 additionalContext 向子代理注入上下文信息:
{
"hookSpecificOutput": {
"hookEventName": "SubagentStart",
"additionalContext": "当前分支是 feature/payment-refactor,请特别关注支付相关的代码"
}
}
这个能力的价值在于自动化上下文注入。比如你有一个 code-reviewer 子代理,每次启动时都需要知道团队编码规范。没有 Hook,你得每次手动提醒“请遵循 camelCase 命名规范”;有了 Hook,这个提醒自动发生:
{
"hooks": {
"SubagentStart": [
{
"matcher": "code-reviewer",
"hooks": [
{
"type": "command",
"command": "echo '{\"hookSpecificOutput\":{\"hookEventName\":\"SubagentStart\",\"additionalContext\":\"请遵循团队编码规范:使用 camelCase,禁止 console.log\"}}'"
}
]
}
]
}
}
这样,每次 code-reviewer 启动时都会自动收到规范——不需要手动提醒,也不占用子代理的 prompt 空间。
SubagentStop:验证子代理的工作成果
SubagentStop 在子代理完成工作后触发。它的决策控制和 Stop 事件完全一致——可以阻止子代理停止,强制它继续工作。
SubagentStop 有一个独特的字段:agent_transcript_path——子代理自己的对话记录。这意味着你的 Hook 脚本可以读取子代理的完整对话历史来判断质量,不是只看最终结果,而是能看到子代理是怎么得出结论的。
实战:验证代码审查质量
下面这个脚本验证 code-reviewer 子代理的审查是否完整——如果它发现了问题但没有给出修复建议,就强制它继续工作:
#!/bin/bash
INPUT=$(cat)
AGENT_TYPE=$(echo"$INPUT" | jq -r '.agent_type')
STOP_HOOK_ACTIVE=$(echo"$INPUT" | jq -r '.stop_hook_active')
TRANSCRIPT=$(echo"$INPUT" | jq -r '.agent_transcript_path')
# 只检查 code-reviewer
if [ "$AGENT_TYPE" != "code-reviewer" ]; then
exit 0
fi
# 防止死循环
if [ "$STOP_HOOK_ACTIVE" = "true" ]; then
exit 0
fi
# 读取子代理的输出,检查是否包含必要的审查要素
if [ -f "$TRANSCRIPT" ]; then
HAS_ISSUES=$(grep -c "issue\|问题\|bug\|warning""$TRANSCRIPT" | true)
HAS_SUGGESTIONS=$(grep -c "suggest\|建议\|recommend""$TRANSCRIPT" | true)
if [ "$HAS_ISSUES" -gt 0 ] && [ "$HAS_SUGGESTIONS" -eq 0 ]; then
cat <<EOF
{
"decision": "block",
"reason": "You found issues but didn't provide suggestions. Please add actionable recommendations."
}
EOF
exit 0
fi
fi
exit 0
三层防护:只检查 code-reviewer → 防止死循环 → 检查是否有问题但没有建议。
当然,用关键词匹配比较粗糙。对于更精细的验证,可以用 Prompt 类型让 LLM 来评估:
{
"hooks": {
"SubagentStop": [
{
"matcher": "code-reviewer",
"hooks": [
{
"type": "prompt",
"prompt": "Evaluate this code review result. Check that: 1) All identified issues have actionable suggestions 2) The suggestions are specific and implementable"
}
]
}
]
}
}
五、实战项目:完整的 Hook 系统
项目一:安全钩子系统
目标:保护敏感资源,防止危险操作,记录审计日志。
.claude/
settings.json
hooks/
block-dangerous.sh # 阻止危险命令
protect-files.sh # 保护敏感文件
audit-log.sh # 记录操作日志
配置:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "./hooks/block-dangerous.sh" }
]
},
{
"matcher": "Write",
"hooks": [
{ "type": "command", "command": "./hooks/protect-files.sh" }
]
},
{
"matcher": "Edit",
"hooks": [
{ "type": "command", "command": "./hooks/protect-files.sh" }
]
}
],
"PostToolUse": [
{
"matcher": "*",
"hooks": [
{ "type": "command", "command": "./hooks/audit-log.sh" }
]
}
]
}
}
三道防线:
命令拦截(PreToolUse → Bash):拦截 rm -rf /、git push --force origin main等灾难性操作文件保护(PreToolUse → Write/Edit):保护 .env等敏感文件不被修改审计日志(PostToolUse → *):所有操作无差别记录,事后追溯
强度递减(拦截→拦截→记录),覆盖面递增(Bash→Write/Edit→所有工具)。这就是经典的纵深防御策略。
项目二:质量钩子系统
目标:自动格式化代码,检查 Lint 错误,确保测试通过。
.claude/
settings.json
hooks/
auto-format.sh # 自动格式化
lint-check.sh # Lint 检查
run-tests.sh # 运行测试
两阶段质量保证:
逐文件质量保证(PostToolUse → Write/Edit):每次写入或编辑后,先格式化再 Lint 全局质量门控(Stop):所有工作完成后运行完整测试套件
注意:同一个 hooks 数组中的多个 Hook 按顺序执行——先格式化再 Lint,顺序不能反。
六、高级模式与最佳实践
多 Hook 链
多个 Hook 按数组顺序依次执行。如果任何一个返回阻止决策,后续 Hook 不会执行。所以应该把“不能失败”的 Hook(如日志)放在最前面。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write",
"hooks": [
{ "type": "command", "command": "./hooks/format.sh" },
{ "type": "command", "command": "./hooks/lint.sh" },
{ "type": "command", "command": "./hooks/log.sh" }
]
}
]
}
}
环境变量
CLAUDE_PROJECT_DIR | |
CLAUDE_SESSION_ID | |
CLAUDE_TOOL_NAME | |
CLAUDE_FILE_PATH | |
CLAUDE_ENV_FILE |
CLAUDE_ENV_FILE 尤其有用。SessionStart Hook 可以向这个文件写入 export 语句,在会话开始时设置全局环境:
#!/bin/bash
# session-setup.sh
if [ -n "$CLAUDE_ENV_FILE" ]; then
echo'export NODE_ENV=development' >> "$CLAUDE_ENV_FILE"
echo'export DEBUG_LOG=true' >> "$CLAUDE_ENV_FILE"
fi
exit 0
异步 Hook
默认 Hook 会阻塞执行。对于耗时操作(完整测试套件、发送通知、调用外部 API),可以用 async: true:
{
"type": "command",
"command": "./hooks/run-tests-async.sh",
"async": true,
"timeout": 300
}
三个限制:只有 command 类型支持异步;不能阻止操作(因为操作在 Hook 完成前已继续);输出在下一轮才传递给 Claude。
HTTP Hook:对接外部服务
如果处理逻辑在远程服务上(团队共享的审计服务、集中式安全扫描平台),用 type: "http":
{
"type": "http",
"url": "http://localhost:8080/hooks/tool-use",
"headers": {
"Authorization": "Bearer $MY_TOKEN"
},
"allowedEnvVars": ["MY_TOKEN"]
}
注意:HTTP 状态码不能阻止操作,必须在 2xx 响应体里用 JSON 表达决策。
七、frontmatter 内置 Hooks——比全局配置更精准
在 Commands 和 Skills 的 frontmatter 中可以包含临时 Hooks(仅在该命令/技能执行期间有效):
---
description:Deploywithsafetychecks
hooks:
-event:PreToolUse
matcher:Bash
command:|
if [ "$TOOL_INPUT" == *"production"* ]; then
echo "Production deployment detected" >&2
fi
-event:PostToolUse
matcher:Edit
command:npxprettier--write"$FILE_PATH"
once:true
---
Deploytheapplicationtostagingenvironment.
once: true 表示 Hook 只触发一次,适合“完成后运行一次测试”这类场景。
子代理 frontmatter Hooks
考虑这个场景:你有一个 db-reader 子代理执行 SQL 查询,想检查 SQL 注入风险。
方案一:全局 settings.json——所有 Bash 命令都会被检查,包括 npm install、git status 这些和 SQL 无关的命令。
方案二:子代理 frontmatter Hooks——只在 db-reader 的 Bash 命令上触发:
---
name:db-reader
description:Read-onlydatabaseexplorer
tools:Read,Grep,Glob,Bash
permissionMode:plan
hooks:
PreToolUse:
-matcher:"Bash"
hooks:
-type:command
command:"./hooks/check-sql-injection.sh"
Stop:
-hooks:
-type:prompt
prompt:"Check if any query results contain PII"
---
Youareadatabaseanalysisspecialist...
关键规则:
支持所有事件类型 Stop 自动转换为 SubagentStop 生命周期绑定子代理 格式与 settings.json 一致
何时用 frontmatter vs 全局?
Hook 只与特定子代理相关?→ frontmatter Hook 需要随子代理分发?→ frontmatter Hook 需要在子代理外也生效?→ 全局 settings.json
八、Hook 工程设计方法论
三维决策框架
设计 Hook 方案时,想清楚三个问题:
| 在哪里拦截? | |
| 用什么方式判断? | |
| 在哪里配置? |
选择原则:能用 command 解决的不用 prompt,能用 prompt 解决的不用 agent,在最小作用域内生效。
Hook + SubAgent 组合模式
完整的组合案例——带质量门控的代码审查子代理:
frontmatter Hook:子代理内部自检(“我知道自己漏了什么”) SubagentStart Hook:外部注入上下文 SubagentStop Hook:外部验收(“它觉得完成了,但其实不够好”)
内部自检和外部验收视角不同,互为补充。
九、调试技巧
1. 使用 stderr 输出调试信息
echo"DEBUG: Processing file $FILE_PATH" >&2
echo'{"decision": "allow"}'# stdout 是 JSON
2. 手动测试 Hook 脚本
echo'{"tool_name": "Bash", "tool_input": {"command": "rm -rf /tmp/test"}}' | ./hooks/block-dangerous.sh
echo"Exit code: $?"
3. 使用 claude --debug 查看完整执行细节
4. 常见问题排查:
Hook 不触发:检查 matcher 是否正确(区分大小写),重启会话 权限问题: chmod +x hooks/*.shJSON 解析错误:确保输出是有效 JSON,检查 shell profile 中的 echo 语句 Stop Hook 死循环:检查是否遗漏 stop_hook_active判断
总结
两讲内容完整覆盖了 Hooks 的全部知识体系:
概念:AI 中间件 配置:六个位置、四种类型 核心事件:PreToolUse、PostToolUse、Stop 高级事件:SubagentStart、SubagentStop 组合系统:从单个 Hook 到多层防线 工程方法论:三维决策框架
Hooks 是 Claude Code 扩展机制中唯一能拦截和修改 AI 行为的组件。用好它,就能在享受 AI 效率的同时,守住安全和质量的底线。
不要等到出了问题再补救,而要未雨绸缪。从 Stop Hook 质量门控到 SubAgent 事件验收,从 frontmatter 精准配置到三维决策框架——每一步都设防,构建滴水不漏的 Hook 工程体系。
记住:不是做完了再检查,而是检查通过了才算做完。
本系列往期文章:
第01章:从被动使用到主动驾驭:Claude Code底层技术全景学习笔记 第02章:告别“失忆症”,用 CLAUDE.md 打造 AI 的长期记忆 第03章:分而治之——Sub-Agents 的核心概念与实战 第04章:从 Sub-Agents 到 Multi-Agent,一套架构方法论搞定90%的AI产品设计 第05章:打造只读型代码审查员,让 AI 不再“好心办坏事” 第06章:噪声隔离实战——让子代理替你过滤测试与日志 第07章:并行探索与流水线编排——让 AI 代理像团队一样协同工作 第08章:Agent Teams——当 AI 智能体学会组队协作 第09章:深入理解 Skills——Agent 生态中的“可操作知识”与触发机制 第10章:任务型 Skills 实战——让重复工作一键直达 第11章:渐进式披露架构设计——让知识在正确的时刻到达正确的地方 第12章:双剑合璧——Skills × SubAgent 协同实战 第13章:纲举目张——Skills 架构定位与高阶能力解析 第14章:燎原之势——Claude Code Skills如何百天定义行业新标准 第15章:防患未然——用Hooks给AI助手装上自动防护盾
如果我的文章对你有帮助,请帮忙点赞、在看、转发一下,你的支持是我持续输出高质量文章的最大动力,非常感谢!
夜雨聆风