Hooks

features/externalHooks 的 20 个生命周期事件、App/Session/Agent 三层分工、退出码 2 的阻断语义,以及 Skills 与 Hooks「建议 vs 规则」的分工。

作者 David更新于 第 20 篇(共 33 篇)

上一篇的 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 适合做提醒和轻量拦截,不该是唯一的安全防线。

两条扩展路径的分工

SkillsHooks
形态Markdown 指令本地命令
作用对象模型(改变它怎么想)系统(改变会发生什么)
能否阻断否能(三个事件)
来源各级目录与插件用户配置与已装插件

Skill 是建议,Hook 是规则。

这个区分是安全模型上的分水岭:模型可以不听 skill,但绕不过 hook。skill 写「不要删除生产数据」,模型大概率会听,但它本质上只是一段提示词——上下文一长、指令一冲突,就可能被稀释。hook 写同样的约束,是在工具真正执行前由一段代码判定,模型没有任何余地。

所以判据很清楚:希望模型「通常这么做」用 skill;要求它「绝不那样做」用 hook。 把硬约束写进 skill,是这类系统里最常见的安全误区。

来源那一行也值得注意:hook 是会在本机执行的命令,除了用户自己写的,还可能来自插件。这正是插件与市场要在安装前把信任级别摆出来的原因。

下一篇:子代理与 swarm。

本页目录