让 coding agent 自己暴露错误:用 Hook 做最短反馈回路
很多 agent 任务拖得很长,并不是改代码慢,而是反馈回来得太晚。它连续改五个文件,最后才跑一次全量测试;这时错误已经和后续改动缠在一起,模型只能靠猜去回溯。更糟的是,测试输出塞进同一个对话后,真正的失败行常被几百行日志淹没。与其在提示词里反复说“每次修改后测试”,不如把这件事变成工具调用后的固定动作。
我现在更愿意把 hook 当成一个很小的反馈控制器:写入发生后,立刻格式化、选一个足够窄的检查、把结果压缩成下一轮能读懂的结论。目标不是每编辑一次就跑完整 CI,那会让 agent 在等待和噪声里耗死;目标是让它在继续扩散改动前,尽早看到局部事实。Claude Code 的 PostToolUse 可以按工具名匹配,在工具成功完成后运行命令;hook 从标准输入拿到本次调用的 JSON 上下文。[1] 这正好适合把“文件刚被改过”变成可执行的质量检查。
先按失败半径设计,而不是按工具设计#
第一层放不会失败或几乎不会失败的整理动作,例如对刚写入的 TypeScript 文件跑 formatter。第二层放定位很准的检查,例如改到 src/auth/ 才跑认证单测,改到 schema 才跑类型检查。全量测试留给一个明确的阶段闸门,例如 agent 宣称任务完成、准备交付 diff 时。三个层次的延迟和价值不同,混成一个 PostToolUse 脚本,最后通常只会得到一个又慢又经常被绕开的 hook。
这里有个容易忽略的细节:hook 是确定性自动化,不是第二个聪明 agent。官方指南把它定位为在特定事件上运行用户定义的 shell 命令,并说明可用 matcher 过滤事件。[2] 因此脚本应该做确定的事:读 JSON,识别路径,调用已有的 formatter 或测试命令,返回简短结果。不要让脚本再调用模型去判断“这次需不需要测试”,否则你只是把不确定性藏进了另一个进程。
失败信息也要有预算。原始测试日志可以写进临时文件,但回传给 agent 的 stdout 最好只保留失败测试名、第一段断言和复现命令。它下一步需要的是“should reject expired token 失败,执行 pnpm test auth 可复现”,不是整段依赖下载日志。若检查超时,明确写 CHECK_SKIPPED_TIMEOUT,不要伪装成通过。没有结论的绿色输出,比红色输出更危险。
快速上手:给写入操作加一个窄检查#
下面以 Claude Code 项目级 hook 为例。先建目录和脚本;命令只读入本地 JSON,并只调用项目已有的 pnpm 脚本。把 pnpm test auth 换成你仓库里已经验证过的窄测试命令:
mkdir -p .claude/hooks
cat > .claude/hooks/after-write-auth.sh <<'SH'
#!/usr/bin/env bash
set -euo pipefail
payload=$(cat)
path=$(printf '%s' "$payload" | jq -r '.tool_input.file_path // empty')
case "$path" in
*src/auth/*|*src/auth/*test*|*src/auth/*spec*)
pnpm exec prettier --write "$path"
if pnpm test auth -- --runInBand; then
printf 'AUTH_CHECK=pass path=%s\n' "$path"
else
printf 'AUTH_CHECK=fail path=%s; rerun: pnpm test auth -- --runInBand\n' "$path" >&2
exit 2
fi
;;
*) printf 'AUTH_CHECK=skip path=%s\n' "$path" ;;
esac
SH
chmod +x .claude/hooks/after-write-auth.sh
再把它绑定到 Write 和 Edit。先使用记录模式两三天,确认路径模式和测试时长;如果每次都稳定在可接受时间内,再让失败阻断后续操作。官方参考说明 command hook 的标准输入是事件 JSON,输出和退出状态可用于向 Claude 返回结果。[1]
{
"hooks": {
"PostToolUse": [{
"matcher": "Write|Edit",
"hooks": [{
"type": "command",
"command": "./.claude/hooks/after-write-auth.sh",
"timeout": 45
}]
}]
}
}
别从“所有文件一律全量测试”开始。先挑一个最容易回归、测试又足够快的边界,比如鉴权、金额计算或序列化。连续观察一周:实际触发次数、平均耗时、失败后是否真的减少后续无关 diff。若 hook 大多是 skip,收窄 matcher;若频繁超时,换成更小的测试目标,或把全量检查移到交付阶段。
还要把 hook 当成仓库接口的一部分来维护。脚本里列出依赖的命令,给它本身写一个输入样例测试;改目录结构时同步修改路径模式。否则半年后它会悄悄不触发,团队仍以为 agent 被保护着。最有用的信号不是“hook 已配置”,而是一次真实失败能在 agent 的下一步之前出现,并让它只修相关的那一小块代码。
可以再加一个很朴素的防呆规则:hook 不修改业务文件,只格式化当前文件,并把检查结果写到标准输出。业务修复仍由 agent 或开发者完成。这样遇到失败时,Git diff 只包含原本的实现和可预期的格式变化,排查不会混进脚本自动生成的补丁。对于会触发网络、生成迁移或重启服务的命令,别挂在每次编辑之后;把它们留在人工确认的发布步骤。Hook 的价值是缩短局部反馈,不是替人扩大自动化权限。
当某个检查连续误报时,也不要马上删掉。先保存十次触发的路径和耗时,看看是路径规则过宽、测试依赖共享状态,还是测试本身不稳定。前两种可以修规则,第三种应该先修测试。能被 agent 稳定执行的检查,首先得是人类在本地也愿意反复执行的检查。
Sources#
[1] https://code.claude.com/docs/en/hooks — Hooks reference | Claude Code Docs [2] https://code.claude.com/docs/en/hooks-guide — Automate actions with hooks | Claude Code Docs