本文档介绍 OneCode Hook 子系统的设计思路、模块总览与使用方式。架构决策与实现细节参见 Hook 模块架构设计。
- 1. 概述
- 2. 设计思路
- 3. 模块总览
- 4. 拦截点清单与边界
- 5. 执行器类型
- 6. 配置文件
- 7. 使用介绍
- 8. 模板插值字段
- 9. 优先级与作用域
- 10. 策略与安全
- 11. 扩展指南
- 12. 调试与排查
- 13. 相关文档
OneCode 的 Hook 模块是核心扩展机制,允许用户在 Agent 执行的关键拦截点注入自定义逻辑,用于 CI/CD 集成、安全策略、自动化通知、审计日志等场景。
模块按"拦截点 × 执行器"两个维度组织:
| 维度 | 含义 | 取值 |
|---|---|---|
| 拦截点(HookInterceptionPoint) | 何时触发 | 6 种拦截点(input / pre_model_call / post_model_call / pre_tool_call / post_tool_call / output) |
| 执行器(HookType) | 如何执行 | 3 种执行器(Command / Notification / Http) |
Hook 系统通过 ~/.onecode/hooks.json 与 <cwd>/.onecode/hooks.json 声明式配置,无需修改代码即可扩展。
边界:Hook 是控制平面(策略裁决 / 阻断 / 改写),不是遥测平面。会话生命周期、Stop 纠偏、上下文压缩、 Goal 编排、权限审批通知与异常收尾由 OneCode 宿主负责,不属于 Hook 拦截范围——详见 §4 边界。
- 声明式配置:通过
hooks.json描述钩子,无需重新编译或修改源码 - 执行器策略模式:每种
HookType对应一个IHookExecutor实现,新增执行器只需实现接口 + DI 注册 - 拦截点 × Matcher 二维过滤:拦截点决定何时触发,matcher 决定是否匹配(如
tool_name == "Bash") - 优先级串行执行:同一拦截点下多个 hook 按 priority 升序串行执行,结果聚合
- 安全优先:工作区不受信任时一律不触发;pre 类拦截点异常 fail-closed
- 可观测可调试:
/hooks命令实时展示注册项、策略状态与拦截点清单
OneCode 运行时
│
├─ 启动 ──────────▶ HookConfigHotReloader.BootstrapAndStartWatching
│ HookConfigBootstrapper 加载 hooks.json → 注册到 HookRegistry
│ → 开始监视配置目录
│
├─ 配置变更 ──────▶ HookConfigHotReloader(FileSystemWatcher + 500ms 防抖)
│ HookConfigBootstrapper.Build 快照 → HookRegistry.ReplaceAll 原子整体交换
│ (解析失败 / 异常时保留上一次有效配置;代次 Generation +1)
│
├─ 拦截点 ────────▶ IHookExecutionService.FireAsync(payload)
│ │
│ ├─ 策略前置检查(工作区信任)
│ ├─ matcher 过滤(拦截点 + matcherValue 两维)
│ ├─ priority 升序排序
│ ├─ 串行执行 IHookExecutor(全部匹配项都执行)
│ ├─ 结果聚合(HookResultAggregator)
│ └─ 清理 once hook
│
└─ 关闭 ──────────▶ (Registry 随进程退出释放)
pre_tool_call / post_tool_call 两个拦截点通过 HookMiddleware 接入 MAF(Microsoft.Agents.AI)函数调用管道:
| 拦截点 | 集成点 | 行为 |
|---|---|---|
pre_model_call / post_model_call |
ModelCallHookDecorator(OneCode.Infrastructure/Ai/,包装 ChatClient,位于 FunctionInvokingChatClient 下方) |
纯审计:流式交付零缓冲(update 逐条透传,post 审计以旁路副本收集);无活跃 hook 时跳过 fire |
pre_tool_call |
HookMiddleware(在 AgentPipelineBuilder 中 .Use() 注册) |
阻断时返回 ToolResult.Error 使该次工具调用失败(不调用 ctx.Terminate,保留批次完整性);异常 fail-closed |
post_tool_call |
同上(next 之后) | 不消费 result,仅做通知/审计;异常 fail-soft |
其余拦截点由 QueryStreamEngine 在外部输入(input)与最终响应交付(output)边界触发。
┌─────────────────────────────────────────────┐
│ hooks.json (声明式配置) │
│ ~/.onecode/hooks.json (priority 100) │
│ <cwd>/.onecode/hooks.json (priority 200) │
└──────────────────────┬──────────────────────┘
│ 启动加载
▼
┌─────────────────────────────────────────────┐
│ HookConfigBootstrapper │
│ HookSettingsLoader 解析 → 注册到 Registry │
└──────────────────────┬──────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ HookRegistry │
│ 按 (Point, Matcher) 二维索引 │
│ O(1) 拦截点查找 + Glob 模式匹配 │
└──────────────────────┬──────────────────────┘
│ GetMatchesForPoint
▼
┌──────────────────┐ ┌─────────────────────────────────┐
│ HookPolicyService│◀──策略检查──── │ HookExecutionService │
│ · 工作区信任 │ │ · 策略前置检查 │
│ │ │ · matcher 过滤 │
│ │ │ · priority 排序 │
│ │ │ · 串行执行 + 结果聚合 │
└──────────────────┘ └──────────────┬──────────────────┘
│ 按 HookType 分发
┌──────────────────────────┼──────────────────────────┐
│ │ │
▼ ▼ ▼
┌──────────────────┐ ┌──────────────────────┐ ┌──────────────────────┐
│ CommandHookExec │ │ NotificationHookExec │ │ HttpHookExecutor │
│ (CliWrap 外部 │ │ (Provider 策略分发) │ │ (IHttpClientFactory) │
│ 进程 + stdin) │ │ │ │ │
└──────────────────┘ └──────────┬───────────┘ └──────────────────────┘
│ IEnumerable<INotificationProvider>
┌───────────┴───────────┐
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────────┐
│ FeishuProvider │ │ WeChatWorkProvider │
│ (飞书机器人) │ │ (企业微信群机器人) │
└──────────────────┘ └──────────────────────┘
| 类型 | 职责 |
|---|---|
HookInterceptionPoint / HookInterceptionPoints |
6 个拦截点枚举与协议词汇(线格式名、matcher 字段、开放范围、内部生命周期节点诊断与旧事件迁移映射) |
HookType |
3 种执行器类型枚举(Command / Notification / Http) |
HookPayload |
钩子数据载荷,传递给执行器的完整上下文 |
HookRegistration |
钩子注册项(Name / Point / Matcher / Priority / Once / ExecutorType / TimeoutMs / Config) |
HookConfig |
单个 Hook 的配置(公共字段 + 类型特有字段) |
HookMatcherGroup |
匹配器分组:一个 matcher pattern 下的一组 hook 配置 |
HookResult / AggregatedHookResult |
执行结果 / 聚合结果 |
HookResultAggregator |
多个 HookResult 合并为单个 AggregatedHookResult |
HookPointMetadata / HookPointMetadataRegistry |
拦截点元数据与 matcher 语义的权威声明(用于 UI 展示和文档生成) |
HookTypeParser |
字符串 → HookType 解析(单一事实源) |
IHookExecutor |
执行器接口,按 HookType 分发 |
IHookExecutionService |
执行服务契约(Core 层接口,App 层实现) |
INotificationProvider / NotificationMessage / NotificationSendResult |
通知渠道接口(Core 层) |
| 类型 | 职责 |
|---|---|
HookRegistry |
钩子注册表,按 (Point, Matcher) 二维索引;ReplaceAll 支持热重载原子整体替换并递增 Generation |
HookExecutionService |
执行服务实现,策略前置 + 过滤 + 排序 + 串行执行 + 聚合 |
HookPolicyService |
策略控制(工作区信任检查:当前目录是否在 trustedDirectories 中) |
GlobHookMatcher |
Glob 风格通配符匹配器 |
HookSettingsLoader |
从独立 hooks.json 加载配置(matcher-group 格式;已迁移 / 不再支持的节点名给出诊断、JSON 错误含行号) |
HookConfigBootstrapper |
启动加载器 + 快照构建器:Bootstrap(启动注册)与 Build(纯读取快照,热重载共用),并把各文件加载状态写入诊断 |
HookConfigHotReloader |
配置热重载器:监视 hooks.json / notification-providers.json 变更,防抖后整体重建注册表(last-good 保护),Dispose 停止监视 |
HookLoadDiagnostics(HookFileLoadDiagnostics.cs) |
进程级最近一次 Bootstrap 的各文件加载状态,/hooks 概览展示 |
CommandHookExecutor |
Command 类型执行器(CliWrap + stdin JSON + exit code 语义) |
NotificationHookExecutor |
Notification 类型执行器(经 NotificationProviderRegistry 渠道解析) |
HttpHookExecutor |
Http 类型执行器(IHttpClientFactory + 模板插值) |
NotificationProviderDefinitionLoader |
声明式 Provider 定义加载器(notification-providers.json;https 校验、preset 展开、无效条目跳过 + 警告) |
NotificationProviderRegistry |
通知渠道解析中枢:声明式定义优先、编译型兜底,同名声明式胜出(实例缓存) |
WebhookNotificationProviderBase |
Webhook 通知渠道基类(飞书/企微共享流程) |
DeclarativeNotificationProvider |
声明式渠道唯一引擎类(消息渲染 → 签名 → 请求 → 成功判定) |
FeishuNotificationProvider |
飞书机器人通知 Provider(编译型) |
WeChatWorkNotificationProvider |
企业微信群机器人通知 Provider(编译型) |
HookTemplateRenderer |
{{Field}} 模板插值渲染器(支持 8 个字段,未知字段保持原样) |
HostStopSessionCloseService |
宿主停止时兜底关闭前台会话的 IHostedService(随 AddHookServices 注册) |
HookSerializerContext |
JSON Source Generator(高频序列化性能) |
敏感字段展开器 HookSecretExpander(dpapi: → DPAPI 解密;否则 ${ENV_VAR} 环境变量展开)位于 OneCode.Infrastructure/Hooks/(OS 级加密适配)。
| 类型 | 职责 |
|---|---|
HookMiddleware |
MAF 函数调用管道中间件,触发 pre_tool_call / post_tool_call |
业务侧触发点:
| 模块 | 触发的拦截点 |
|---|---|
QueryStreamEngine |
input(进运行循环前,可阻断)、output(最终响应交付前,可阻断) |
ModelCallHookDecorator(Infrastructure/Ai,包装 ChatClient) |
pre_model_call / post_model_call(模型调用边界,纯审计) |
HookMiddleware(Infrastructure) |
pre_tool_call / post_tool_call(MAF 函数调用管道) |
不再由 Hook 覆盖的产品能力(保留宿主实现,非拦截点):会话启动/结束(
SessionManager)、 Stop 纠偏与预算终结(QueryStreamEngine)、上下文压缩(CompactService)、 Goal 编排(GoalDecomposer)、权限审批通知(ApprovalBroker)、异常分类告警(可观测性平面)。 这些路径在hooks.json中写旧事件名会得到明确诊断,不会被静默忽略。
6 种拦截点,每种有对应的 matcher 字段和可选值(权威声明见 HookPointMetadataRegistry):
| 拦截点 | 触发时机 | matcher 字段 | 可选值 | 可阻断 |
|---|---|---|---|---|
input |
交互式输入进入 agent run 前(StreamInteractiveAsync,被阻断时不写会话历史) |
无 matcher | — | ✅ deny |
pre_model_call |
一次模型请求发送前 | model_id(glob) |
模型标识(如 gpt-* / claude-*) |
❌(纯审计) |
post_model_call |
一次模型完整响应返回后(工具调用执行前) | model_id(glob) |
同上 | ❌(纯审计) |
pre_tool_call |
工具调用执行前 | tool_name(glob) |
Bash / Edit / Write / Read / Grep / Glob / Task / todos_*(Harness 待办工具) / mcp__*(MCP 工具名为 mcp__{server}__{tool})等 |
✅ deny |
post_tool_call |
工具调用成功或失败返回后 | tool_name(glob) |
同上 | ❌ |
output |
流式生成完毕、最终响应落盘与收尾前(不缓冲流式 token delta,deny 时跳过最终响应落盘、发射 ErrorEvent 并以 RunTerminalReason.Blocked 终结本轮) |
无 matcher | — | ✅ deny |
model 节点是纯审计语义:
pre_model_call/post_model_call的裁决结果(含 exit code 2)被丢弃, 不阻断、不改写模型调用——需要阻断的策略应挂在input/pre_tool_call/output。
退出码约定(Command 类型):
| 退出码 | 语义 | stdout / stderr 行为 |
|---|---|---|
0 |
成功(或 JSON 显式阻断) | stdout 为 JSON 则解析为 HookResult(含 blockingError 时触发阻断,含 additionalContext 时在 input 节点并入本轮输入);非 JSON 降级存入 HookResult.Message(仅作诊断载荷,不向 TUI 聊天流输出) |
2 |
阻断(deny) | stderr 封装为 BlockingError,在 input / pre_tool_call / output 节点阻止后续操作并向模型或用户报错 |
| 其他 | 非阻断错误 | stderr 记入 HookResult.Message(Outcome = NonBlockingError),不阻止后续操作,也不打断或污染 TUI 对话流 |
边界(刻意不做):
| 能力 | 归属 | 说明 |
|---|---|---|
| 会话启动 / 结束 | SessionManager |
产品会话边界 ≠ agent run 边界,不冒充 agent_startup / agent_shutdown |
| Stop 纠偏续跑 | QueryStreamEngine |
output deny 直接终结本轮,不重入;纠偏、预算终结由宿主负责 |
| 上下文压缩 | Harness / CompactService |
标准协议无 compact 节点 |
| Goal 编排 | GoalDecomposer |
直连模型路径,不构造 agent 生命周期 |
| 权限审批通知 | ApprovalBroker |
审批平面 ≠ Hook 控制平面 |
| 异常分类告警 | 可观测性 | 被动遥测,不是策略裁决 |
pre_tool_call 参数改写(transform) |
未建 | 首期仅裁决契约:deny 走工具错误载荷,不写回工具参数 |
output 响应改写(transform) |
未建 | output deny 直接终结本轮,不写回最终响应内容 |
evaluate_only 执行模式 |
未建 | 无「只评估不阻断」开关;阻断与否由 exit code 2 裁决 |
| 统一拦截记录摘要流 | 未建 | 审计由 hook 自身(通知 Provider / 外部命令)落地,无统一的拦截记录摘要流 |
通过 CliWrap 执行外部进程,stdin 传入 JSON 格式的 HookPayload。
配置字段(type / timeout / once / priority / statusMessage 是三种执行器类型的公共字段,下面各节不再重复):
| 字段 | 类型 | 说明 |
|---|---|---|
type |
string? |
执行器类型(command / notification / http),省略时按 command 处理 |
command |
string |
必填。Shell 命令(Windows: cmd.exe /c;Unix: /bin/sh -c) |
timeout |
int? |
超时毫秒数,默认 5000 |
once |
bool |
只执行一次:成功执行后自动移除(Blocking 送达阻断裁决同样移除,防止同一拦截点被反复触发);异常 / 取消 / 执行器缺失视为未完成,保留待下次触发 |
priority |
int? |
优先级(越小越先执行),默认按配置目录推导 |
statusMessage |
string? |
通知消息标题(仅 Notification 类型消费,见 §5.2);Command / Http 类型忽略 |
stdin Payload 示例(HookPayload 的 JSON 序列化):
{
"point": "pre_tool_call",
"sessionId": "abc123",
"cwd": "/home/user/project",
"toolName": "Bash",
"toolInput": { "command": "rm -rf /" },
"timestamp": "2026-07-18T10:00:00+00:00"
}stdout 返回 JSON 控制(exit code 0 时):
{
"additionalContext": "注入到本轮输入的额外信息(仅 input 拦截点消费)",
"message": "可选诊断文本(仅保留在聚合结果中,不写入 TUI 对话流)"
}阻断(exit code 2):
{
"blockingError": { "error": "dangerous command", "command": "check-dangerous.py" }
}示例:
{
"type": "command",
"command": "python3 ~/.onecode/scripts/audit.py"
}通过 INotificationProvider 策略分发到外部消息系统(飞书/企业微信等)。
配置字段:
| 字段 | 类型 | 说明 |
|---|---|---|
provider |
string |
必填。Provider 名称:feishu / wechat_work |
webhookUrl |
string |
必填。渠道提供的接入地址 |
secret |
string? |
签名密钥(HMAC-SHA256,可选) |
message |
string |
消息内容模板,支持 {{field}} 插值 |
statusMessage |
string? |
通知消息标题(作为 Provider 的 Title;声明式 Provider 模板可用 {{Title}} 引用) |
timeout |
int? |
超时毫秒数,默认 5000 |
敏感字段保护(${ENV_VAR} / dpapi:):webhookUrl / secret(Notification)、url / headers 值(Http)、声明式定义的 url 均支持两种保密形式,运行时由 HookSecretExpander 展开(先展开敏感字段,再做 {{field}} 模板插值):
${ENV_VAR}:展开进程环境变量,未定义的变量展开为空串。如"secret": "${FEISHU_SIGN_SECRET}";dpapi:前缀:值为 Windows DPAPI(CurrentUser)保护的 Base64 串,运行时解密。生成方式(PowerShell):
$bytes = [Text.Encoding]::UTF8.GetBytes('your-plain-secret')
[Convert]::ToBase64String([Security.Cryptography.ProtectedData]::Protect($bytes, $null, 'CurrentUser'))
# → hooks.json 中写 "secret": "dpapi:<上一步输出的 Base64>"解密失败按非阻断错误处理(不会泄露明文)。建议含密钥的配置文件加入 .gitignore,不入库。
示例:
{
"type": "notification",
"provider": "feishu",
"webhookUrl": "https://open.feishu.cn/open-apis/bot/v2/hook/xxx",
"secret": "your-sign-secret",
"message": "[OneCode] 拦截点 {{Point}} 触发于 {{Timestamp}}"
}支持的 Provider:
| Provider 名称 | 渠道 | 消息格式 | 签名算法 |
|---|---|---|---|
feishu |
飞书机器人 | {"msg_type":"text","content":{"text":"..."}} |
HMAC-SHA256(key = timestamp + "\n" + secret, msg = "") → Base64 |
wechat_work |
企业微信群机器人 | {"msgtype":"text","text":{"content":"..."}} |
HMAC-SHA256(key = secret, msg = timestamp + "\n" + secret) → Base64 |
dingtalk |
钉钉机器人(声明式内置默认,见 §5.4) | {"msgtype":"text","text":{"content":"..."}} |
HMAC-SHA256(key = timestamp(ms) + "\n" + secret, msg = "") → Base64 → URL 编码 |
通用 HTTP 调用,用于 webhook 通知、CI/CD 触发、自定义服务集成、审计回调等场景。
配置字段:
| 字段 | 类型 | 说明 |
|---|---|---|
url |
string |
必填。请求目标 URL,支持 {{field}} 插值;亦支持敏感字段展开(${ENV_VAR} / dpapi:,见 §5.2) |
method |
string? |
HTTP 方法(GET / POST / PUT / DELETE / PATCH),默认 POST |
headers |
Dictionary<string, string>? |
自定义请求头,值支持 {{field}} 插值与敏感字段展开(如 "Authorization": "Bearer ${CI_TOKEN}") |
body |
string? |
请求体模板(POST/PUT/PATCH/DELETE 使用),支持 {{field}} 插值 |
timeout |
int? |
超时毫秒数,默认 5000 |
HTTP 韧性(刻意不重试):Notification 与 Http 的出站调用不做自动重试、无熔断——
timeout(默认 5s)是单次调用的唯一超时权威,失败一律降级为 NonBlockingError(不阻断主流程);onceHook 失败后保留注册,下次事件自然补发。理由:出站 POST 目标(自定义 webhook/CI 触发)幂等性无法保证,重试决策交给配置方业务层。
示例:
{
"type": "http",
"method": "POST",
"url": "https://ci.example.com/api/trigger",
"headers": {
"Authorization": "Bearer {{Token}}",
"X-Point": "{{Point}}"
},
"body": "{\"point\":\"{{Point}}\",\"tool\":\"{{ToolName}}\",\"cwd\":\"{{Cwd}}\",\"timestamp\":\"{{Timestamp}}\"}"
}与 Notification 的区别:
| 维度 | Http | Notification |
|---|---|---|
| 目标 | 通用 HTTP 调用(自定义 URL/Method/Headers/Body) | 消息推送业务场景(飞书/企微等固定渠道格式) |
| 灵活性 | 高(完全自定义) | 低(渠道特定格式) |
| 签名 | 自行通过 headers 实现 | Provider 内置 HMAC 签名 |
| 响应解析 | 仅判断 HTTP 状态码 | 解析渠道特定响应字段(code/errcode 等) |
对于「固定端点 + 特定 payload + 签名变体」类渠道(钉钉/自建网关等),无需写 C#——用 JSON 定义描述渠道,由唯一引擎类 DeclarativeNotificationProvider 在运行时执行。
文件位置与三层覆盖(同名整条替换,高层胜出,Info 日志):
<AppContext.BaseDirectory>/notification-providers.json ← 内置默认(随应用打包,当前含 dingtalk)
~/.onecode/notification-providers.json ← 用户级
<cwd>/.onecode/notification-providers.json ← 项目级(优先级最高)
Schema(以钉钉为例,内置默认即此定义):
| 字段 | 说明 |
|---|---|
displayName |
渠道显示名(诊断展示用) |
method |
HTTP 方法,默认 POST |
url |
必填。默认端点,必须 https://;发送时 hooks.json 的 webhookUrl 优先 |
headers |
附加请求头;值支持消息模板 |
body |
必填。JSON 请求体模板;字符串叶子支持消息模板,其他类型保持原样 |
signing |
签名定义,或 {"preset": "..."} 简写(见下);preset 存在时其余 signing 字段被忽略 |
success |
响应成功判定:顶层字段 field 与 equals 相等即成功;缺省仅看 HTTP 2xx |
签名定义(signing 完整写法):
| 字段 | 说明 |
|---|---|
keyTemplate / messageTemplate |
HMAC key / message 模板;变量仅 {Secret} / {Timestamp} / {TimestampMs},禁止表达式求值 |
encoding |
base64(默认)/ base64-urlencoded(钉钉)/ hex |
placement |
query(默认)/ header(header 放置时签名与时间戳自动附加为请求头) |
paramName |
签名参数名,默认 sign |
timestampParamName |
时间戳参数名,默认 timestamp |
timestampMs |
true 时 URL / Header 中的时间戳参数取毫秒(钉钉要求毫秒);签名模板变量 {Timestamp} 恒为 Unix 秒,毫秒用 {TimestampMs} |
签名 preset(官方算法固化为三选一):
| preset | 渠道 | 算法 |
|---|---|---|
feishu |
飞书 | HMAC(key = timestamp秒\nsecret, msg = "") → Base64 |
wechat_work |
企业微信 | HMAC(key = secret, msg = timestamp秒\nsecret) → Base64 |
dingtalk |
钉钉 | HMAC(key = timestamp毫秒\nsecret, msg = "") → Base64 → URL 编码 |
消息模板字段(body 字符串叶子与 headers 值):{{Text}} / {{Title}} / {{Event}}(拦截点线格式名,如 pre_tool_call)/ {{Timestamp}}(yyyy-MM-dd HH:mm:ss);单花括号写法同样接受,未知占位符保持原样。第一级 payload 渲染(config.message 的 {{Point}} 等 HookPayload 字段)仍由 Notification 执行器完成。
校验规则:url 非 https / 缺 body / 未知 preset 的定义跳过并记录 Warning,不影响同文件其他条目;JSON 错误含行号。
渠道解析顺序:声明式定义优先,编译型 INotificationProvider 兜底,同名声明式胜出(NotificationProviderRegistry)。OAuth、多步交互等复杂集成仍走编译型(见 §11.2)。
| 文件 | 作用域 | 基础优先级 |
|---|---|---|
~/.onecode/hooks.json |
用户级(全局) | 100 |
<cwd>/.onecode/hooks.json |
项目级 | 200 |
两个文件都会被加载,项目级 hook 与用户级 hook 共存,按 priority 升序串行执行。
hooks.json 的根对象即为 hooks 内容(按拦截点名分组),支持两种格式:
每个拦截点下是 matcher 分组数组,每个分组包含 matcher 和 hooks:
{
"pre_tool_call": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "echo 'About to run Bash'",
"timeout": 3000
}
]
},
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "notification",
"provider": "feishu",
"webhookUrl": "https://open.feishu.cn/open-apis/bot/v2/hook/xxx",
"message": "即将写入文件: {{Cwd}}"
}
]
}
],
"post_tool_call": [
{
"matcher": "*",
"hooks": [
{
"type": "http",
"method": "POST",
"url": "https://audit.example.com/api/tool",
"body": "{\"tool\":\"{{ToolName}}\",\"point\":\"{{Point}}\"}"
}
]
}
],
"pre_model_call": [
{
"matcher": "gpt-*",
"hooks": [
{
"type": "command",
"command": "echo 'model request'"
}
]
}
],
"output": [
{
"hooks": [
{
"type": "notification",
"provider": "wechat_work",
"webhookUrl": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx",
"message": "AI 响应完成于 {{Timestamp}}"
}
]
}
]
}HookSettingsLoader 只解析 matcher-group 格式。拦截点下直接平铺 hook 配置数组(无 matcher/hooks 包装)的旧格式会被跳过(反序列化后 Hooks 为空),不会自动包装为 matcher=""。
旧版 OneCode 专有事件名不再有效,加载时给出明确诊断(不静默跳过):
| 旧事件名 | 诊断 |
|---|---|
PreToolUse / PostToolUse / UserPromptSubmit |
提示迁移到 pre_tool_call / post_tool_call / input |
SessionStart / SessionEnd / Stop / StopFailure / PreCompact / PostCompact / GoalStageInvoke / Notification |
提示"不再属于 Hook 拦截范围",并说明对应能力仍由宿主负责 |
matcher 字段使用 Glob 风格通配符:
| Pattern | 语义 | 示例 |
|---|---|---|
"" 或 "*" |
匹配所有(wildcard) | 任何 tool_name 都触发 |
"Bash" |
精确匹配(大小写不敏感) | 仅 tool_name == "Bash" 触发 |
"Bash*" |
前缀通配 | Bash / BashRun 都触发 |
"*Tool" |
后缀通配 | MyTool / Tool 都触发 |
"Write|Read" |
管道分隔多值 | Write 或 Read 触发 |
" Write | Read " |
自动 trim 空白 | 等价于 Write|Read |
注意:matcher 字段名因拦截点而异(工具节点为
tool_name,模型节点为model_id)。详见 §4 拦截点清单与边界。无 matcher 的拦截点(input/output)使用""或"*"。
当前没有 hooks.* 策略开关配置项。唯一的策略控制是工作区信任:Hook 仅在当前工作目录位于 settings.json 的 trustedDirectories 列表(含子目录)中时才会触发(见 §10.1)。
hooks.json 与 notification-providers.json 支持运行时热重载,修改后无需重启 OneCode(PR-5 C3)。
机制(HookConfigHotReloader):
AppStartupService预热时经BootstrapAndStartWatching完成初始 Bootstrap 并开始监视用户(~/.onecode)与项目(.onecode)配置目录顶层(目录不存在则跳过该层,无热重载)- 文件变更(Changed / Created / Deleted / Renamed)经 500ms 防抖合并后触发一次重建
- 重建走
HookConfigBootstrapper.Build产出不可变快照,再经HookRegistry.ReplaceAll原子整体交换(不做增量 Bootstrap) - 编程注册的 hook(名称不带
config:前缀,如插件 API 注册项)在整体交换中原样保留 - 成功后同步刷新
HookLoadDiagnostics(/hooks概览实时反映最新状态)与NotificationProviderRegistry声明式定义
失败语义(last-good 保护):
| 场景 | 行为 |
|---|---|
| 任一层 hooks.json 解析失败(JSON 损坏 / IO 错误 / 根节点非对象) | 保留上一次有效配置,诊断记录 Failed,LogWarning |
| 构建过程抛出意外异常 | 保留上一次有效配置,LogError |
| hooks.json 被删除 | 正常路径:清空该层 config hook(NotFound 参与整体交换) |
| 已废弃事件名 / 内部生命周期拦截点名 | 非致命:跳过该条目并写入加载诊断(已废弃事件名给出迁移提示,内部生命周期拦截点提示不在开放范围) |
未知 type(HookType) |
非致命:跳过该条目并记 Warning 日志(不写入加载诊断),其余条目照常生效 |
注意:
config:前缀注册名是热重载区分配置 hook 与编程注册 hook 的依据,自定义 hook 的注册名请勿使用该前缀。
| 子命令 | 语法 | 说明 |
|---|---|---|
| 无参数 | /hooks |
概览:持久 hook 数 + 配置代次 + 工作区信任状态 + 配置文件路径 + 各 hooks.json 最近一次加载诊断(状态 / hook 数 / 解析错误) |
list / ls |
/hooks list |
完整 hook 列表(按 source 分组:Managed / User / Project / Plugin) |
events |
/hooks events |
可用拦截点列表(含 matcher 字段) |
status |
/hooks status |
Hook 策略状态(工作区信任) |
/hooks 概览示例:
Hooks: agent interception points
Persistent hooks: 3
Config generation: 1
Workspace trusted: yes
Config files:
~/.onecode/hooks.json (user-level, priority 100)
.onecode/hooks.json (project-level, priority 200)
Load diagnostics (last bootstrap):
~/.onecode: Loaded (2 hooks)
<cwd>/.onecode: NotFound (0 hooks)
Subcommands: /hooks list | /hooks events | /hooks status
/hooks list 示例:
User hooks:
pre_tool_call:
[100] Command config:pre_tool_call:Command:abc123
command: echo 'About to run Bash'
post_tool_call:
[100] Http config:post_tool_call:Http:def456
Project hooks:
output:
[200] Notification config:output:Notification:ghi789
阻止 rm -rf / 等危险命令执行:
{
"pre_tool_call": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3 ~/.onecode/scripts/block-dangerous.py",
"timeout": 2000
}
]
}
]
}block-dangerous.py 读取 stdin JSON,检测到危险命令时 exit 2:
import json, sys
payload = json.load(sys.stdin)
cmd = payload.get("toolInput", {}).get("command", "")
if "rm -rf /" in cmd:
sys.stderr.write("Blocked: dangerous command detected")
sys.exit(2)
sys.exit(0){
"post_tool_call": [
{
"matcher": "Bash",
"hooks": [
{
"type": "notification",
"provider": "feishu",
"webhookUrl": "https://open.feishu.cn/open-apis/bot/v2/hook/xxx",
"secret": "your-sign-secret",
"message": "[OneCode] 工具 {{ToolName}} 执行完成 @ {{Timestamp}}"
}
]
}
]
}{
"post_model_call": [
{
"matcher": "gpt-*",
"hooks": [
{
"type": "http",
"method": "POST",
"url": "https://ci.example.com/api/onecode/model-response",
"headers": {
"Authorization": "Bearer ci-token-xxx"
},
"body": "{\"session\":\"{{SessionId}}\",\"cwd\":\"{{Cwd}}\",\"ts\":\"{{Timestamp}}\"}"
}
]
}
]
}{
"pre_model_call": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "echo '[audit] model request' >> ~/.onecode/audit.log",
"timeout": 2000
}
]
}
]
}{
"output": [
{
"hooks": [
{
"type": "notification",
"provider": "wechat_work",
"webhookUrl": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx",
"message": "✅ OneCode 响应完成 @ {{Timestamp}}"
}
]
}
]
}压缩审计、会话启动通知、错误停止告警等能力已移出 Hook 拦截范围(见 §4 边界), 请改用可观测性通道或宿主产品能力。
业务模块可通过注入 IHookExecutionService 直接触发拦截点:
public class MyService
{
private readonly IHookExecutionService? _hooks;
public MyService(IHookExecutionService? hooks) => _hooks = hooks;
public async Task DoWorkAsync(CancellationToken ct)
{
if (_hooks is null) return;
var payload = new HookPayload
{
Point = HookInterceptionPoint.Input,
Cwd = Environment.CurrentDirectory,
UserMessage = "some user prompt",
};
// actualMatcherValue 为对应拦截点的 matcher 字段值
// (如 pre_tool_call 传 tool_name,pre_model_call 传 model_id)
await _hooks.FireAsync(payload, actualMatcherValue: null, ct: ct);
}
}运行时动态注册 hook(如插件系统):
public class MyPlugin
{
private readonly HookRegistry _registry;
public MyPlugin(HookRegistry registry) => _registry = registry;
public void RegisterHook()
{
_registry.Register(new HookRegistration
{
Name = "my-plugin:audit",
Point = HookInterceptionPoint.PostToolCall,
Matcher = "Bash",
Priority = 150, // User 级
ExecutorType = HookType.Command,
TimeoutMs = 3000,
Config = new HookConfig
{
Type = "command",
Command = "echo 'Bash executed'",
},
});
}
}Notification 和 Http 类型执行器支持 {{Field}} 模板插值;可插值字段限于下表 8 个(HookPayload 的其余字段不参与插值):
| 字段 | 类型 | 说明 |
|---|---|---|
{{Point}} |
string |
拦截点线格式名(如 pre_tool_call) |
{{SessionId}} |
string |
会话 ID |
{{Cwd}} |
string |
当前工作目录 |
{{ToolName}} |
string |
工具名称(仅工具相关拦截点) |
{{UserMessage}} |
string |
用户消息(input 拦截点) |
{{AgentId}} |
string |
Agent ID |
{{AgentType}} |
string |
Agent 类型 |
{{Timestamp}} |
string |
触发时间戳(格式 yyyy-MM-dd HH:mm:ss) |
插值规则:
- 未知字段保持原样(如
{{Unknown}}不被替换) - 字段值为 null 时替换为空字符串
- 大小写敏感(必须与上表完全一致)
示例:
{
"url": "https://api.example.com/{{Point}}",
"headers": { "X-Tool": "{{ToolName}}" },
"body": "{\"cwd\":\"{{Cwd}}\",\"ts\":\"{{Timestamp}}\"}"
}| 优先级范围 | 来源 | 说明 |
|---|---|---|
0-99 |
Managed(系统内置) | 系统级 hook |
100-199 |
User(用户级) | ~/.onecode/hooks.json 加载,默认 priority 100 |
200-299 |
Project(项目级) | <cwd>/.onecode/hooks.json 加载,默认 priority 200 |
300+ |
Plugin(插件) | 插件运行时注册 |
同一拦截点下多个匹配的 hook 按 priority 升序串行执行(数值越小越先执行)。执行结果通过 HookResultAggregator 聚合:
| 字段 | 聚合策略 |
|---|---|
BlockingErrors / AdditionalContexts(列表) |
累加 |
Message(字符串) |
last-write-wins(单个 hook 的 Message 优先于其 SystemMessage,跨 hook 取最后一个非空值) |
once: true 的 hook 成功执行(Success 或 Blocking)后自动从 Registry 移除,适合"仅首次触发"的场景(如初始化提示)。异常、取消、执行器缺失视为未完成,保留待下次触发。
Hook 仅在受信任工作区中触发。HookPolicyService.IsCurrentWorkspaceTrusted() 检查当前工作目录是否在 settings.json 的 TrustedDirectories 列表中(支持子目录继承)。
未受信任的工作区中所有 hook 都不会触发,避免恶意仓库通过 hooks.json 执行任意命令。
当前没有 disableAll / allowManagedOnly / strictPluginOnly 等策略开关。工作区信任(§10.1)是唯一的策略门控。
| 场景 | 策略 | 说明 |
|---|---|---|
| Pre-hook(pre_tool_call)异常 | fail-closed | 异常转为 ToolResult.Error 使该次工具调用失败(不调用 ctx.Terminate,保留批次完整性) |
| Post-hook(post_tool_call)异常 | fail-soft | 仅记日志,保留原工具结果返回 |
| 单个 hook 执行器异常 | 隔离 | 异常被吞掉记 Warning,其他 hook 继续执行 |
OperationCanceledException |
透传 | 保留取消信号,不吞掉 |
每个 hook 独立超时(默认 5000ms,通过 timeout 字段配置)。超时后:
- Command 类型:返回
NonBlockingError结果 - Http 类型:返回
NonBlockingError结果 - Notification 类型:返回
NonBlockingError结果
超时不影响其他 hook 的执行。
DI 注册类真相源:
src/OneCode.App/Services/Hooks/HookServiceCollectionExtensions.cs的AddHookServices()。 按下述步骤增改代码即可,无需修改其他文件(组合根OneCodeApp.Create已调用AddHookServices)。
如需支持新的执行器类型(如 Webhook / Redis):
- 扩展
HookType枚举(OneCode.Core/Hooks/HookTypes.cs):
public enum HookType
{
Command,
Notification,
Http,
Webhook, // 新增
}- 更新
HookTypeParser(OneCode.Core/Hooks/HookTypeParser.cs):
public static HookType Parse(string? type) => type?.ToLowerInvariant() switch
{
"command" => HookType.Command,
"notification" => HookType.Notification,
"http" => HookType.Http,
"webhook" => HookType.Webhook, // 新增
null => HookType.Command,
_ => throw new ArgumentException($"Unknown hook type: '{type}'. Valid values: command, notification, http, webhook."),
};- 实现
IHookExecutor(新文件放OneCode.App/Services/Hooks/)——参照已有的CommandHookExecutor:
public sealed class WebhookHookExecutor : IHookExecutor
{
public HookType Type => HookType.Webhook;
public async Task<HookResult?> ExecuteAsync(
HookPayload payload, HookConfig config, CancellationToken ct)
{
// 实现调用逻辑;null 表示无结果
return null;
}
}- DI 注册(
src/OneCode.App/Services/Hooks/HookServiceCollectionExtensions.cs的AddHookServices):
services.AddSingleton<IHookExecutor, WebhookHookExecutor>();分发机制:
HookExecutionService经IEnumerable<IHookExecutor>注入,按每个执行器的Type属性分发—— 不是 Keyed Services。新增执行器只需追加一行AddSingleton<IHookExecutor, X>()。
如需支持新的通知渠道,优先用声明式定义(零代码,见 §5.4)——固定端点 + 特定 payload + 签名变体类渠道(钉钉/自建网关等)只需在 ~/.onecode/notification-providers.json 写一条定义即可,钉钉已随应用内置默认。仅 OAuth、多步交互等复杂集成才需要写 C#:
- 实现
INotificationProvider(推荐继承WebhookNotificationProviderBase):
public sealed class SlackNotificationProvider(HttpClient httpClient, ILogger<SlackNotificationProvider>? logger = null)
: WebhookNotificationProviderBase(httpClient, logger)
{
public override string Name => "slack";
// ... 渠道特定 payload / 签名 / 响应解析
}- DI 注册(
src/OneCode.App/Services/Hooks/HookServiceCollectionExtensions.cs的AddHookServices):
services.AddSingleton<INotificationProvider, SlackNotificationProvider>();
services.AddHttpClient<SlackNotificationProvider>();- 使用(
hooks.json):
{
"type": "notification",
"provider": "slack",
"webhookUrl": "https://hooks.slack.com/services/xxx",
"message": "拦截点 {{Point}} 触发 @ {{Timestamp}}"
}编译型与声明式并存时,
NotificationProviderRegistry按名称解析:声明式定义优先,同名声明式胜出。
拦截点集合与 AGENT-HOOKS-0.1 协议对齐,不再自由扩展。如需新增拦截点(如 OnTokenBudgetExceeded / PreFileWrite):
- 扩展
HookInterceptionPoint枚举(OneCode.Core/Hooks/HookInterceptionPoint.cs)并同步HookInterceptionPoints的线格式名、matcher 字段与开放集合 - 更新
HookPointMetadataRegistry(OneCode.Core/Hooks/HookPointMetadata.cs)添加拦截点元数据 - 在业务模块触发:注入
IHookExecutionService并调用FireAsync
新增拦截点前先确认它确实是 agent execution seam(输入 / 模型 / 工具 / 输出四类边界之一)。 产品会话生命周期、压缩、Goal 编排、审批通知、异常告警不属于协议拦截范围,不应伪装成新拦截点。
排查步骤:
-
运行
/hooks status检查策略状态:Workspace trusted: NO→ 工作区未受信任,需在settings.json的TrustedDirectories中添加当前目录
-
运行
/hooks list确认 hook 已注册:- 未出现 → 检查
hooks.json路径与 JSON 语法(注意:只支持 matcher-group 格式,见 §6.2)
- 未出现 → 检查
-
检查 matcher:
- 运行
/hooks events确认拦截点的 matcher 字段名 - 确认
actualMatcherValue与 matcher pattern 匹配(如Bashvsbash大小写不敏感)
- 运行
-
确认拦截点已在产品路径接线:
input/output只在QueryStreamEngine主链路生效;pre_tool_call/post_tool_call只对走AgentPipelineBuilder的工具型 Agent 生效 (Main / Worker / Goal 子目标)。GoalDecomposer等直连模型路径不构造 agent 生命周期, 不触发任何拦截点。
- Command 类型:手动执行
command字段,确认退出码与输出 - Http 类型:检查 URL 可达性、Headers 格式、Body JSON 合法性
- Notification 类型:检查
webhookUrl有效、secret正确、Provider 名称匹配
- 仅
input/pre_tool_call/output支持阻断(exit code 2);pre_model_call/post_model_call/post_tool_call是审计语义,deny(exit code 2)不会改变主流程 - 确认 exit code 为
2(其他非零退出码视为非阻断错误) - 查看 stderr 输出(阻断时显示给模型或用户)
Hook 执行相关日志通过 ILogger 输出,关键日志类别:
| 日志类别 | 关键消息 |
|---|---|
HookExecutionService |
Hook execution skipped: workspace not trusted(Debug 级) |
HookExecutionService |
Hook '{Name}' execution error |
CommandHookExecutor |
Command hook execution failed(超时以结果消息 Command hook timed out after {N}ms 呈现,不写日志) |
HttpHookExecutor |
HTTP hook timed out after {N}ms / HTTP hook execution failed |
NotificationHookExecutor |
Notification provider '{Provider}' not registered |
HookConfigBootstrapper |
Bootstrapped {Count} hooks total |
- Hook 模块架构设计 — 架构决策、数据模型、实现细节
- 设置文档 - Hooks 配置 —
hooks.json与策略开关 - 命令文档 - /hooks —
/hooks命令完整说明 - Permission vs ToolApproval vs Filter — MAF 函数调用管道中 Permission / ToolApproval / 观测性中间件的职责边界与数据流
{ "dingtalk": { "displayName": "钉钉机器人", "method": "POST", "url": "https://oapi.dingtalk.com/robot/send", "body": { "msgtype": "text", "text": { "content": "{{Text}}" } }, "signing": { "preset": "dingtalk" }, "success": { "field": "errcode", "equals": 0 } } }