Renovate 仓库的 Claude Code Hooks:用会话钩子守护 Agent 开发流程
Renovate 仓库的 Claude Code Hooks:用会话钩子守护 Agent 开发流程
本文聚焦 Renovate 仓库中 tools/agents/hooks/ 目录下的一组 Claude Code 钩子脚本,系统讲解它们如何围绕 SessionStart、CwdChanged、PreToolUse、Stop 四个生命周期事件,为 AI Agent 的编码会话自动完成环境初始化、工具链约束与提交前质量门禁,并深入剖析其底层实现与测试依据,帮助读者理解并复刻一套可落地的 Agent 会话治理方案。
为什么需要 Agent Hooks
Renovate 是一个跨平台的依赖自动化 CLI 项目,代码库规模庞大:lib/modules/ 下包含数十个 datasource、manager 与 versioning 模块,开发时依赖 mise 管理语言运行时、pnpm 管理 Node 依赖、vitest 跑测试。当 Claude Code 等 AI Agent 以会话方式接入这种仓库时,会面临三个典型问题:
- 环境缺失:Agent 启动会话时,
mise声明的语言运行时或pnpm依赖尚未安装,工具调用会直接失败; - 工具滥用:Agent 可能习惯性地执行
npm install、yarn或jest,与仓库约定的pnpm/vitest工作流冲突; - 质量失控:Agent 结束会话时,可能留下未通过 lint / 类型检查 / 测试的改动。
tools/agents/hooks/readme.md 说明,tools/agents/hooks/ 目录存放的就是 Claude Code 在会话关键节点自动调用的钩子脚本,目的正是把上述问题变成可自动执行的规则。下面按钩子逐一展开。
Hook 全景:四个生命周期事件
| 钩子脚本 | 触发事件 | 作用 |
|---|---|---|
sessionstart-check.ts |
SessionStart |
会话启动/恢复/清空/压缩时安装工具与依赖 |
cwdchanged-check.ts |
CwdChanged |
切换工作目录(含 worktree)时按需初始化新目录 |
pretooluse-check.ts |
PreToolUse |
每次工具调用前拦截被禁用的包管理器与测试器 |
stop-check.ts |
Stop |
会话结束前对改动文件执行 pnpm check --all 门禁 |
钩子脚本都采用 顶层 await 的 ESM 脚本 形态,直接 import 同目录 utils/ 下的工具模块,运行时不依赖编译产物,保持轻量与快速启动。
SessionStart:会话启动即环境就绪
触发时机
sessionstart-check.ts 在会话启动、恢复(resume)、清空(clear)或压缩(compact)时运行一次。这些场景对应 Claude Code 的 SessionStart 事件,其标准输入(stdin)会携带 source 字段,取值为 startup、resume、clear、compact 之一,由 tools/agents/hooks/utils/schemas.ts 中的 SessionStartHookInput 用 z.enum(...) 严格校验。
实现:两段式 provisioning
脚本主体极简——只有一行调用:
import { provision } from './utils/provision.ts';
await provision();
真正的逻辑在 tools/agents/hooks/utils/provision.ts:
export async function provision(cwd?: string): Promise<void> {
const opts: Options = cwd ? { cwd } : {};
try {
await exec('mise', ['install'], opts);
} catch {
console.error('mise is not installed or failed — skipping');
}
await exec('pnpm', ['install'], opts);
}
执行顺序与容错策略与 readme 描述完全一致:
mise install:安装.tool-versions/ mise.toml 中声明的语言运行时(Node 等)。非致命:失败只打印警告并继续,保证没有安装mise的环境也能进入会话;pnpm install:安装 Node 依赖。致命:失败会直接阻断会话——因为后续所有工具调用都依赖node_modules。
这种"运行时非致命、依赖致命"的分级策略值得借鉴:语言运行时缺失时 Agent 仍可能完成部分静态分析类工作,而依赖缺失会让一切编译、测试无从谈起。
执行细节:环境透传
provision 内部的 exec 来自 tools/agents/hooks/utils/exec.ts,它包装了仓库工具层的 execBase,做了两件关键事:
- 把 stdout/stderr 都重定向到
process.stderr,避免钩子的正常输出污染 Claude Code 解析的 stdout 协议流; - 注入
RENOVATE_AGENT_HOOK: '1'环境变量,让被调用的命令感知自己运行在 Agent 钩子上下文中,方便仓库内其他逻辑区分执行来源。
CwdChanged:切换目录后的按需初始化
触发时机与输入校验
cwdchanged-check.ts 在每次工作目录变化(包括进入 worktree)时运行。它从 stdin 读取 JSON,并先用 CwdChangedHookInput(schemas.ts)做校验——所有 hook 事件共有的 session_id、transcript_path、cwd、hook_event_name 字段由 BaseHookInput 定义,hook_event_name 被 z.literal('CwdChanged') 固定。解析失败会打印错误并以退出码 1 终止,保证协议输入严格可预期。
核心逻辑:只初始化"未安装"的新检出
const root = await getRepoRoot(result.data.cwd);
if (!root || (await pathExists(join(root, 'node_modules')))) {
process.exit(0);
}
await provision(root);
这里有一个精心设计的优化:并非每次 cd 都重新安装。只有当前目录能解析出仓库根(git rev-parse --show-toplevel),且该根目录下不存在 node_modules(即一个全新的 worktree 检出)时,才会针对新根目录执行 provision;主检出已安装或普通目录切换则直接 no-op 退出。这一判断由 utils/git.ts 的 getRepoRoot 与 node:fs/promises 的 access 配合完成。
这样既保证了切换到未初始化的 worktree 或子模块时环境可用,又避免了在已就绪的检出上做重复劳动。
PreToolUse:把"用 pnpm"变成强制规则
触发时机
pretooluse-check.ts 在每次工具调用前运行,是四个钩子中执行频率最高的。它同样从 stdin 读取 JSON,用 PreToolUseHookInput 校验——目前是一个基于 tool_name 的 discriminated union,只定义了 Bash 工具(schemas.ts),tool_input.command 为待执行的命令字符串。
拦截规则与正则
if (parsed.tool_name === 'Bash') {
const { command } = parsed.tool_input;
if (/(?:^|\s)(?:npm|npx|yarn)(?:\s|$)/.test(command)) {
deny('Use pnpm instead of npm/npx/yarn');
}
if (/(?:^|\s)pnpm\s+(?:run\s+|exec\s+)?jest(?:\s|$)/.test(command)) {
deny('Use pnpm vitest instead of pnpm jest');
}
}
两条规则的语义:
- 禁用
npm/npx/yarn:正则以词边界(^、\s、$)匹配独立命令令牌,避免误伤npm出现在路径或参数中的情况,凡命中即拒绝,并要求改用pnpm; - 禁用
pnpm ... jest:允许pnpm本身,但拦截pnpm run jest、pnpm exec jest等形式的 jest 调用,要求改用pnpm vitest——这与 Renovate 仓库统一使用 vitest 的测试约定(参见 vitest.config.mts)一致。
拒绝输出协议
拒绝动作由 utils/output.ts 的 deny 完成,它向 stdout 输出符合 Claude Code 规范的 JSON:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Use pnpm instead of npm/npx/yarn"
}
}
permissionDecision: "deny" 会直接阻止该次 Bash 调用,理由字符串回传给 Agent 供其调整策略。源码中留有 oxlint 豁免注释,说明这里刻意不引入 lib/util/schema-utils 的 Json 工具——因为那会拖入整个 lib 的导入依赖图(logger、yaml、toml),违背钩子"依赖轻、启动快"的设计原则。
Stop:结束会话前的质量门禁
触发时机
stop-check.ts 在 Claude Code 即将停止时运行,是会话收尾的最后一道防线。
获取改动文件
它先通过 utils/git.ts 的 getChangedFiles() 计算自基线分支以来的改动文件:
- 优先取
git merge-base origin/main HEAD作为基线; - 若
origin/main不可用,回退到上游分支@{u}; - 再不可用则回退到
HEAD。
然后用 git diff --name-only --diff-filter=ACMR <base> 列出新增、复制、修改、重命名的文件(忽略删除),得到本次会话真正需要检查的改动集合。
门禁执行与阻断输出
if (changedFiles.length > 0) {
try {
await exec('pnpm', ['check', '--all', ...changedFiles]);
} catch {
block('pnpm check --all failed — the issues must be resolved before finishing');
}
}
pnpm check --all 是 Renovate 针对改动文件的综合检查入口(lint / 类型 / 格式化 / 测试等,参见 package.json 的 scripts 定义)。只有改动非空才执行,避免空会话空跑;一旦检查失败,block(output.ts)会输出 { "decision": "block", "reason": ... },Claude Code 收到后阻止会话停止,并提示 Agent 先修复问题再结束。
从源码到协议:钩子如何与 Claude Code 通信
四个钩子共享同一套通信模式,理解它就能自行扩展新钩子:
| 方向 | 机制 |
|---|---|
| 输入 | Claude Code 把事件 JSON 写入钩子进程的 stdin,脚本用 utils/stdin.ts 以 UTF-8 逐块读取后 JSON.parse |
| 校验 | 用 utils/schemas.ts 中基于 Zod v4 定义的 schema 做 safeParse,非法输入直接报错退出 |
| 输出 | 需要干预时向 stdout 打印一行 JSON:PreToolUse 用 deny 拒绝单次调用,Stop 用 block 阻断会话停止;正常通过时不输出 |
| 命令执行 | 统一走 utils/exec.ts 封装的 execa 执行器,重定向 stderr 并注入 RENOVATE_AGENT_HOOK=1 |
其中 schema 层完整覆盖了 Claude Code 四种事件的结构:SessionStart 的 source 枚举、Stop 的 permission_mode 枚举(default / plan / acceptEdits / auto / dontAsk / bypassPermissions)、PreToolUse 的 Bash 参数(command、description、timeout、run_in_background)等,均被精确建模,保证钩子对协议输入的解码安全可靠。
测试保障:钩子行为可验证
钩子不是一次性脚本,而是带有配套测试的可维护代码。tools/agents/hooks/ 目录下每个钩子都有对应的 *.spec.ts 测试(如 cwdchanged-check.spec.ts、pretooluse-check.spec.ts、stop-check.spec.ts),utils/ 下也有 git.spec.ts、provision.spec.ts 等单元测试。测试覆盖了 stdin 解析、schema 校验、目录判断、正则拦截等关键路径,使得"环境初始化、工具约束、质量门禁"这套 Agent 治理逻辑与仓库其他代码一样可回归、可演进。
小结:一套可复刻的 Agent 会话治理范式
Renovate 仓库这套 tools/agents/hooks/ 设计,本质上把"AI Agent 在本仓库内该如何工作"沉淀成了四条可自动执行的纪律:
- 启动即就绪(SessionStart):
mise install+pnpm install,运行时失败容忍、依赖失败阻断; - 切换即补位(CwdChanged):仅对新 worktree 等未安装检出做按需初始化,避免重复劳动;
- 执行即约束(PreToolUse):正则拦截
npm/npx/yarn/jest,强制pnpm/vitest工作流; - 收尾即验收(Stop):对改动文件跑
pnpm check --all,不通过不许结束。
任何使用 Claude Code 协助开发大型 Node.js/TypeScript 仓库的团队,都可以参照 tools/agents/hooks/readme.md 与 tools/agents/hooks/ 下的源码与测试,为自己的仓库搭建一套"环境自动初始化 + 工具调用约束 + 提交前质量门禁"的 Agent 开发护栏。