Hooks
features/externalHooks 的 20 个生命周期事件、App/Session/Agent 三层分工、退出码 2 的阻断语义,以及 Skills 与 Hooks「建议 vs 规则」的分工。
上一篇的 Skills 用 Markdown 改变模型怎么想。这一篇的 Hooks 是另一条扩展路径——在生命周期节点跑本地命令,改变系统会发生什么。
怎么用
hook 写在 config.toml(位于 ~/.kimi-code/ 或 KIMI_CODE_HOME)的 [[hooks]] 数组里,schema 见 features/externalHooks/configSection.ts:10:
export const HookDefSchema = z
.object({
event: z.enum(HOOK_EVENT_TYPES),
matcher: z.string().optional(),
command: z.string().min(1),
timeout: z.number().int().min(1).max(600).optional(),
})
.strict();.strict() 意味着多写一个字段,配置就通不过校验。一个最小例子(示意):
[[hooks]]
event = "PreToolUse"
matcher = "Bash"
command = "node ~/.kimi-code/hooks/guard-bash.mjs"
timeout = 5插件也能在清单里声明 hook,加载时与配置里的合并在一起。
目录结构:三层作用域
v0.29 时代 hook 挂在 session/hooks/(HookEngine 那一套);现在是 agent-core-v2 的一个 Feature:
features/externalHooks/
├── app/externalHooksRunnerService.ts App:读配置与插件 hook、按事件索引、执行
├── session/sessionExternalHooksService.ts Session:会话起停、心跳、子代理起停
├── agent/agentExternalHooksService.ts Agent:工具、权限、输入、回合、压缩、任务
└── internal/ 匹配、起进程、解析结果名字里的 external 是为了和引擎内部的 hook 区分:src/hooks.ts 的 OrderedHookSlot 是一条可按 before/after 排序的中间件链,循环、工具执行器等服务都用它开放扩展点(如 loop.hooks.onDidFinishStep)。用户配置的外部 hook,正是挂在这些内部扩展点和事件总线上的。
20 个事件
internal/types.ts:3:
export const HOOK_EVENT_TYPES = [
'PreToolUse',
'PostToolUse',
'PostToolUseFailure',
'PermissionRequest',
'PermissionResult',
'UserPromptSubmit',
'UserPromptQueued',
'TurnStarted',
'Stop',
'StopFailure',
'Interrupt',
'SessionStart',
'SessionEnd',
'SessionHeartbeat',
'SubagentStart',
'SubagentStop',
'TaskStarted',
'PreCompact',
'PostCompact',
'Notification',
] as const;v0.29 时是 16 个,v0.32.0 加了 UserPromptQueued、TurnStarted、SessionHeartbeat、TaskStarted 四个。
| 组 | 事件 | 能干什么 |
|---|---|---|
| 工具 | PreToolUse / PostToolUse / PostToolUseFailure | 调用前拦截、调用后审计、失败时告警 |
| 权限 | PermissionRequest / PermissionResult | 审批前后通知,记录谁批了什么 |
| 输入 | UserPromptSubmit / UserPromptQueued | 输入进模型前注入或拦截;观察排队 |
| 回合 | TurnStarted / Stop / StopFailure / Interrupt | 区分正常收敛、异常中止、用户取消 |
| 会话 | SessionStart / SessionEnd / SessionHeartbeat | 起停时准备与清理;每 60 秒心跳 |
| 子代理与任务 | SubagentStart / SubagentStop / TaskStarted | 观测子代理与后台任务 |
| 压缩 | PreCompact / PostCompact | 在压缩前后落存档 |
| 其它 | Notification | 后台任务状态变化时通知外部系统 |
终止事件仍然分开,呼应回合收敛那条要求:Stop 在模型正常准备收尾时触发,StopFailure 在回合失败时触发,Interrupt 在回合被取消(如用户按 Esc)时触发。合成一个事件,外部脚本就没法分辨「用户喊停」和「模型跑飞了被掐断」。
能阻断,不只是旁观
脚本从 stdin 读到一份 JSON(键名由 camelToSnake 转成 hook_event_name、session_id、tool_name 这类形状),靠退出码表态。internal/runHook.ts:140:
function resultFromExitCode(exitCode: number, stdout: string, stderr: string): HookResult {
if (exitCode === 2) {
const message = stderr.trim();
return {
action: 'block',
message,
reason: message,
stdout,
stderr,
exitCode,
};
}退出码 2 表示阻断,stderr 就是理由;退出码 0 时 stdout 若是 JSON 且 hookSpecificOutput.permissionDecision 为 deny,同样算阻断。能真正改变流程的只有三个事件:
PreToolUse:在工具执行前的否决链上投票。UserPromptSubmit:阻断则本轮不调模型;放行时 stdout 包成<hook_result>附进上下文。Stop:阻断理由作为一条 user 消息追加,模型接着干;每个回合只给一次机会,防止无限续跑。
PreToolUse 的接线在 agent/agentExternalHooksService.ts:160:
private registerToolHooks(toolExecutor: IAgentToolExecutorService): void {
this._register(
toolExecutor.onBeforeExecuteTool(async (event) => {
const reason = await this.runPreToolUse(event);
if (reason !== undefined) {
event.veto(denyToolExecution(reason));
}
}),
);权限闸门、计划模式守卫也监听同一个 onBeforeExecuteTool,任何一个 veto 都会让工具不执行。需要用户审批的请求经 waitUntil 推迟到所有监听器跑完之后才弹出,所以 PreToolUse 总在审批弹窗之前给出结论。v1 的 pre-tool-call-hook.ts 权限策略已不复存在。
其余事件都是观察型的,脚本返回什么都不改变主流程。
匹配与执行
matcher 是正则(new RegExp(pattern).test(value)),不填则全匹配,正则写错视为不匹配。匹配对象随事件而定:工具事件是工具名,UserPromptSubmit 是输入里各段文本拼成的字符串,SessionStart 是 startup 或 resume。
执行上有几条约定:命中的 hook 并行运行;同一 cwd 下命令相同的只跑一次;默认超时 30 秒,超时先 SIGTERM、100ms 后 SIGKILL;起进程失败、超时、被取消都按放行处理——fail-open。这决定了 hook 适合做提醒和轻量拦截,不该是唯一的安全防线。
两条扩展路径的分工
| Skills | Hooks | |
|---|---|---|
| 形态 | Markdown 指令 | 本地命令 |
| 作用对象 | 模型(改变它怎么想) | 系统(改变会发生什么) |
| 能否阻断 | 否 | 能(三个事件) |
| 来源 | 各级目录与插件 | 用户配置与已装插件 |
Skill 是建议,Hook 是规则。
这个区分是安全模型上的分水岭:模型可以不听 skill,但绕不过 hook。skill 写「不要删除生产数据」,模型大概率会听,但它本质上只是一段提示词——上下文一长、指令一冲突,就可能被稀释。hook 写同样的约束,是在工具真正执行前由一段代码判定,模型没有任何余地。
所以判据很清楚:希望模型「通常这么做」用 skill;要求它「绝不那样做」用 hook。 把硬约束写进 skill,是这类系统里最常见的安全误区。
来源那一行也值得注意:hook 是会在本机执行的命令,除了用户自己写的,还可能来自插件。这正是插件与市场要在安装前把信任级别摆出来的原因。
下一篇:子代理与 swarm。