别让 agent 每改一行都跑全量测试:按变更路由 Hook 验收

· 约 4 分钟阅读

让 coding agent 在每次 Edit 之后都跑一次全量测试,听起来很严谨,实际常常是在制造噪声。[unverified] 它刚改完一个按钮文案,终端就开始跑二十分钟的集成测试;等结果回来,模型已经忘了刚才为什么要改这行。[unverified] 更糟的是,慢检查被频繁触发后,人和 agent 都会学会无视它。[unverified] 更管用的做法是把改动路径、风险级别和最小验证命令绑在一张路由表上,哪怕它只有几条 case。[unverified]

Hook 很适合放这层判断。Claude Code 的 PostToolUse 在工具调用成功后触发,可以按工具名匹配;文档还给出了针对 Write|Edit 运行检查脚本的配置形式。[1] GitHub Copilot 的 agent hook 同样提供 postToolUse,配置文件放在仓库的 .github/hooks/ 目录,默认分支上的文件才会被云端 coding agent 使用。[3] 两边的字段不同,但工程方法一样:把“改了什么”先归类,再决定验证范围。

别按语言路由,按失败半径路由#

很多团队第一版脚本会写成“改了 TypeScript 就跑 pnpm test”。这仍然太粗。[unverified] 一个 src/ui/ 的 CSS 改动、一个 API handler 的参数改动、一个数据库迁移,失败半径完全不同。[unverified] 我通常先给目录分三档:纯展示层只做格式、类型和相关快照;业务模块跑该包的单测;契约、迁移、鉴权、依赖锁文件直接升级到集成测试或阻止自动继续。[unverified]

脚本输出行动建议就够了,不必替 agent 做最终裁决。例如改了 packages/payments/,脚本可以返回“必须跑 payments 测试,且不能跳过”;碰到 migrations/ 时返回“停止,请人工确认回滚方案”。这样 agent 不会把一次昂贵的验证误当成对所有小改动的默认惩罚,也不会在高风险路径里自作主张地继续。[unverified]

Claude Code 的 hook 匹配器能精确匹配 Edit|Write,也可以用正则匹配工具名;PostToolUse 的输出还能作为额外上下文回到会话。[1] 所以路由脚本不必自己扫描整个 Git diff。[unverified] 先读取本次事件里的文件路径,快速决定检查;只有升级到提交前门禁时,再由 CI 做完整 diff 审核。[unverified] 快路径服务于当前回合,慢路径服务于合并,这两个目标别混在一个 hook 里。[unverified]

快速上手:先把检查收窄到三条路径#

下面的例子只增加一个脚本和一份 Claude Code 配置,不会修改应用代码。先建脚本:

mkdir -p .claude/hooks
cat > .claude/hooks/route-check.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

input=$(cat)
file=$(printf '%s' "$input" | jq -r '.tool_input.file_path // empty')

case "$file" in
  packages/web/*)
    pnpm --filter web lint && pnpm --filter web test -- --runInBand
    msg="已运行 web 的 lint 和单测:$file"
    ;;
  packages/api/*)
    pnpm --filter api test
    msg="已运行 api 单测:$file"
    ;;
  migrations/*|pnpm-lock.yaml|package.json)
    msg="高风险改动:$file。不要继续自动修改;请运行 CI 集成测试并复核影响范围。"
    ;;
  *)
    msg="未命中测试路由:$file。只记录本次改动,提交前由 CI 做全量验证。"
    ;;
esac

jq -nc --arg msg "$msg" \
  '{hookSpecificOutput:{hookEventName:"PostToolUse",additionalContext:$msg}}'
EOF
chmod +x .claude/hooks/route-check.sh

再把它接到 PostToolUse。命令里用项目目录变量,避免从子目录启动 agent 时找不到脚本:

{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Edit|Write",
      "hooks": [{
        "type": "command",
        "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/route-check.sh",
        "async": true
      }]
    }]
  }
}

把 JSON 存为 .claude/settings.json 后,先用一段最小输入在本地验证脚本,别直接拿生产代码去试:

printf '%s' '{"tool_input":{"file_path":"packages/web/src/Button.tsx"}}' \
  | .claude/hooks/route-check.sh | jq .

官方示例说明异步 PostToolUse hook 可以在文件改动后运行测试;异步结果会在完成后返回给会话。[1] 但别因为能异步就把所有测试都塞进去。我的经验是把目标定成“十几秒内给出局部结果”;超过这个时间的检查留给 PR 或 CI。否则 agent 会在旧测试还没结束时继续改下一处,最后收到一串已经失去上下文的失败信息。[unverified]

把升级条件写成代码,而不是提示词#

路由表最容易腐化的地方,是目录结构变了,脚本还在把新模块归为默认分支。[unverified] 给每个高风险目录留一条显式 case,并在仓库测试里覆盖它:输入路径,断言选中的命令或升级消息。[unverified] 新增服务时,PR 模板要求同时修改路由测试。[unverified] 这样漏配会在代码评审里暴露,而不是等 agent 恰好改到那里才发现。[unverified]

还要区分“检查失败”和“路由失败”。前者意味着改动可能有问题,应该把测试输出交给 agent 修;后者意味着脚本不知道该跑什么,应该升级人工判断。不要用默认全量测试掩盖路由缺口,那会让每一次未知改动都变慢,团队也看不见规则已经过期。[unverified]

Copilot 的 hook 文档建议分别提供 Bash 与 PowerShell,并要求配置带 version: 1;它也建议用管道输入在本地测试 hook 和检查退出码。[3] 如果你把这类检查下沉到子代理,子代理的 frontmatter 也能定义 hook,作用域只在该子代理运行期间;插件形式的子代理会忽略 hooks、mcpServers 和 permissionMode 字段。[2] 同时支持 Claude Code 与 Copilot 时,把决策逻辑保留在一个可执行脚本里,各自只写很薄的适配层。别复制两套 case 规则,半年后它们一定会分叉。

最后留一条硬底线:局部 hook 只能缩短反馈回路,不能代替合并门。类型检查、受影响包测试和格式化可以在 agent 回合里尽早跑;跨服务集成、迁移演练和安全扫描仍应由 CI 在受控环境中执行。慢验证留在该慢的地方,快验证才不会被拖慢。[unverified]

Sources#

[1] https://docs.anthropic.com/en/docs/claude-code/hooks — Claude Code Hooks reference [2] https://docs.anthropic.com/en/docs/claude-code/sub-agents — Claude Code custom subagents [3] https://docs.github.com/en/copilot/how-tos/use-copilot-agents/coding-agent/use-hooks — GitHub Copilot agent hooks