Coding Code 提供可插拔的钩子点,用户可以在关键节点注入自定义逻辑。本文档介绍所有钩子点、回调签名、触发 API 和用户钩子配置。
共 12 个。表里列出的每个点在生产代码里都有真实触发点;type 列是该点有意义的钩子类型(用户配置里写错类型不会报错,但决策不会被消费)。
| 钩子点 | 触发时机 | type |
|---|---|---|
tool.execute.before |
工具执行前(已通过审批) | observer |
tool.execute.after |
工具执行成功后 | observer |
tool.execute.error |
工具执行失败后 | observer |
tool.approval.pre |
审批决策前(第 3 层) | decision |
tool.approval.post |
审批决策后(审计,含 decision 与 layers) |
observer |
工具被拒绝时不触发 tool.execute.*(工具没被执行),拒绝结果从 tool.approval.post 的 decision.type === 'deny' 读取。
| 钩子点 | 触发时机 | type |
|---|---|---|
agent.turn.start |
轮次开始 | observer |
agent.step.before |
每个推理步骤前 | decision(返回值当前未被消费) |
agent.turn.stop |
本轮无工具调用、准备停止时裁决 | decision |
agent.turn.end |
轮次最终结束(status: done/error/aborted/maxSteps) |
observer |
| 钩子点 | 触发时机 | type |
|---|---|---|
agent.subagent.spawn.before |
子智能体创建前 | decision(可 deny) |
agent.subagent.spawn.after |
子智能体创建后 | observer |
agent.subagent.complete |
子智能体完成时 | observer |
钩子以子进程形式运行,payload 是一份 JSON,所以签名只描述数据的形状:
type ObserverHandler = (payload: Record<string, unknown>) => Effect.Effect<void, never, any>;
type DecisionHandler = (
payload: Record<string, unknown>
) => HookDecision | null | Promise<HookDecision | null>;
interface HookDecision {
decision?: 'allow' | 'deny' | 'ask' | 'continue';
reason?: string;
injection?: string; // 注入到 LLM 上下文的文本
modifiedInput?: Record<string, unknown>; // 修改工具调用参数
}Decision 钩子的返回语义:
allow:直接放行,跳过后续审批层deny:拒绝,附带reasonask:要求用户确认continue:在tool.approval.pre上表示「不干预,继续到下一层」null:不干预(多个 decision 钩子按 priority 升序取首个非 null)
每个 payload 都带 projectPath —— 它是钩子作用域的定位键(见下),也是 hook 脚本判断「我在哪个项目里跑」的依据。
HookService 是 Effect Service,只有三个方法:
| 方法 | 说明 |
|---|---|
emit(point, payload) |
触发该点上所有 observer 钩子;单个钩子抛错只记日志,不带垮整轮 |
emitDecision(point, payload) |
触发该点上所有 decision 钩子,按 priority 升序取首个非 null |
reloadUserHooks(projectPath) |
重新解析该项目的 YAML 配置并重建注册表 |
没有代码级注册 API:钩子只有 YAML 一个来源。运行时每次 emit 都用 payload 里的 projectPath 查注册表,查不到就是空表(no-op)。
钩子按层解析,字段级合并:
- project —
.codingcode/hooks.yaml - global —
~/.codingcode/hooks.yaml
两层都用 name 对齐。项目层只覆盖它显式声明的字段,其余字段继承全局。所以「在项目里关掉一个只在全局定义的钩子」只需写一条最小补丁:
hooks:
- name: log-llm-calls
enabled: false同一层内按 priority 升序执行,数值小的先跑。
| 级别 | 路径 |
|---|---|
| 全局 | ~/.codingcode/hooks.yaml |
| 项目 | .codingcode/hooks.yaml |
hooks:
- name: audit-log
description: 记录每次审批结果
point: tool.approval.post
type: observer
command: node
args: ["./scripts/audit.js"]
priority: 10
- name: block-dangerous-commands
description: 阻止危险命令
point: tool.approval.pre
type: decision
command: node
args: ["./scripts/check-command.js"]
env:
BLOCKED_COMMANDS: "rm,rmdir,format"
priority: 100| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
name |
string |
是 | 钩子名称,跨层对齐与开关都靠它 |
description |
string |
否 | 描述 |
point |
HookPoint |
是 | 钩子点名称(上表 12 个之一) |
type |
'observer' | 'decision' |
是 | 决定是否读 stdout |
command |
string |
是 | 可执行文件 |
args |
string[] |
否 | 命令参数 |
env |
Record<string, string> |
否 | 追加到 process.env 之上 |
priority |
number |
否 | 升序执行,默认 0 |
enabled |
boolean |
否 | 缺省(不写)等于启用;false 表示禁用 |
- payload 以 JSON 写入子进程 stdin,随后关闭 stdin
type: decision时读 stdout 并JSON.parse;退出码非 0、超时(30 秒)、解析失败一律降级为nulltype: observer忽略 stdout 与退出码,只保证跑完command/args/env里的${VAR}目前不做展开(mcp.yaml会展开,两者不一致)
hooks:
- name: slow-tool-alert
point: tool.execute.after
type: observer
command: node
args: ["./scripts/slow.js"]// scripts/slow.js
let raw = '';
process.stdin.on('data', (c) => (raw += c));
process.stdin.on('end', () => {
const { toolName, durationMs, projectPath } = JSON.parse(raw);
if (durationMs > 5000) console.error(`[slow] ${toolName} took ${durationMs}ms in ${projectPath}`);
});hooks:
- name: block-rm-rf
point: tool.approval.pre
type: decision
command: node
args: ["./scripts/check-command.js"]// scripts/check-command.js
let raw = '';
process.stdin.on('data', (c) => (raw += c));
process.stdin.on('end', () => {
const { toolName, args } = JSON.parse(raw);
if (toolName === 'execute_command' && String(args?.command ?? '').includes('rm -rf')) {
process.stdout.write(JSON.stringify({ decision: 'deny', reason: '禁止递归强制删除' }));
}
// 否则什么都不输出 ⇒ 视为 null,不干预
});agent.turn.stop 返回 continue 且带 injection 时,injection 会作为 system 消息写入会话并续行(受 maxStopContinuations 限制,默认 3):
process.stdout.write(JSON.stringify({ decision: 'continue', injection: '还没跑测试,继续。' }));