Files
deepseek-harness/packages/hooks/hook-protocol/README.zh.md
2026-07-28 19:48:59 -07:00

6.6 KiB
Raw Blame History

@deepseek-ai/dsh-hook-protocol

English | 中文

Claude CodeCodex hook 协议格式的共享核心。它不是 cordis 插件:不注册也不注入任何内容。它是一个,提供两个桥接插件(@deepseek-ai/dsh-hooks-claude@deepseek-ai/dsh-hooks-codex)导入的方言无关原语,使两者都无需重复实现协议中相同的部分。

共享 lib 存在的原因是Codex 有意重新实现了 Claude Code hook 协议的一个子集,包括相同的 hooks.json matcher group 形状、相同的退出码stdout 输出契约以及相同的 command hook 执行模式。真正共享的部分位于此处;每个桥接只拥有不同之处。

共享内容(此处)与每方言内容(桥接)

关注点 此处(dsh-hook-protocol 桥接(dsh-hooks-claude / -codex
Matcher 校验 + 测试 matcherDiagnostic(pattern, mode) 用于解析时诊断;compileMatchers(patterns, mode) 用于配置生命周期内的重复匹配;matchesMatcher(pattern, query, mode) 用于一次性的收敛匹配 选择自身原生正则 modeclaude = JavaScriptcodex = Rust regex),拒绝带有诊断的配置组,并在 teardown 时释放已编译集合
运行 hook runHook(bash, hook, opts, now):通过 ctx.bash 提供 stdin payload + env再解码 构造每个事件的 stdin payload + 该方言的 env
解码输出 parseHookOutput(exit, stdout, stderr) → 中性 HookOutput 将中性 HookOutput 映射到 seam 特定的类型化 Decision
合并 N 个 hook mergeHookOutputs(outputs) → 最严格的 MergedHookOutcome (无)
持久记录 appendHookInvoked / appendHookResulthook/* 会话事件;结果的 decisionstderrSummary 从此处的 HookOutput 派生) 在每次调用前后调用它们
脱离运行完全停稳 createDetachedRuns():跟踪发射后不再等待的运行链;drain() 先 abort再等待它们 signal 传给每个脱离的 runHook,并将 drain 注册为 effect disposer

原语

  • matcherDiagnostic(matcher, mode) / compileMatchers(matchers, mode) / matchesMatcher(matcher, query, mode):缺失、'''*' 时匹配全部;两种方言都将纯 [A-Za-z0-9_|]+ pattern 视为按 pipe 分隔的精确多选。其他 pattern 会用原生方言编译为未锚定正则Claude Code 使用 JavaScriptCodex 使用 Rust regex(包括 (?i) 等内联 flag。桥接解析器会丢弃没有 matcher 匹配对象的事件所带 matcher 字段,再使用 matcherDiagnostic 在注册任何 hook 之前拒绝实际会被消费的无效正则,并输出稳定诊断。每个桥接通过 compileMatchers 将配置中每个唯一 pattern 只编译一次,在各 hook 点重复使用,并在插件 teardown 时先 drain 脱离运行,再释放这个有限集合;因此 RustWASM 分配器不会因每次匹配都抬高且无法收缩的内存高水位。matchesMatcher 保留为收敛的一次性谓词,运行时无效 pattern 仍是不匹配而非异常。
  • runHook(bash, hook, options, now):要求并转发调用方拥有的 options.signal,将 options.payload 序列化到 hook stdin当且仅当 options.trailingNewline 时添加尾随换行符),在执行器凭证清理后合并 options.envdsh-bash 受信任插件表层),遵循 hook 的 timeoutSec(否则使用 options.defaultTimeoutMs;默认值属于桥接,其配置默认为 lib 的 DEFAULT_HOOK_TIMEOUT_MS 10 分钟参考值),再解码结果(将 options.expectedEventName 传递给 codec。因此取消会到达执行器的进程组终止与 join 边界。它绝不抛出异常:执行器拒绝(基础设施故障)会变为 HookOutput,其 exitCode: undefined(非阻塞错误)。now 会被注入,以便测试持续时间。
  • parseHookOutput(exitCode, stdout, stderr, expectedEventName?) 解码退出状态与结构化 stdout。退出码 2 使用 stderr 阻塞;其他失败不阻塞。匹配的 hook 特定权限决策会覆盖遗留顶层决策;事件判别字段不匹配或缺失只会抑制事件特定字段。顶层字段仍与事件无关,成功但非 JSON 的输出会留给桥接处理。
  • mergeHookOutputs(outputs):折叠在一个点上匹配的每个 hook 结果:权限优先级为 deny > ask > allow,首个 continue:false 使 halt 粘滞,阻塞原因用 \n\n 连接,additionalContextsystemMessages 按顺序累积。
  • createDetachedRuns():为脱离运行的 emit 形状点跟踪完全停稳(没有 seam 等待它们)。桥接会跟踪每条运行链,包括 hook 运行及其 continuation并将 drain() 注册为 effect disposer。drain 会触发 tracker 的 abort signal(因此仍在运行的 hook 进程会通过 runHook 终止,而不是等待到超时),随后在所有已跟踪链结算后 resolve。因此 fiber.dispose() resolve 时,没有脱离 hook 工作会留下并触发已 dispose 的上下文(见 防御模式dispose 必须达到完全停稳)。

hook/* 会话事件

通过 declaration merging 合并到 SessionEventMap(仅日志,与 compact/* 相同;不是 SurfaceEventType,没有 surfaceOphook/invokedhook 命令已运行)与 hook/result(其结果,按 handlerId 配对,由 appendHookResult 拥有决策规则。Payload 与每事件 JSDoc 位于生成的 持久化日志事件目录stderrSummary 会截断到记录的 stderrSummaryMaxChars(桥接配置,参考默认值 DEFAULT_STDERR_SUMMARY_MAX_CHARS = 500为空时省略

Hook 溯源记录必须位于开启轮次内。轮次中点(PreToolUsePostToolUseStop)按构造满足这条由所有方定义的关系。SessionStart 与轮次前的 UserPromptSubmit 准入 seam 没有 hook/* 记录;获准的上下文改由其带来源的 user/message 作为证据,详见 hooks Agent Note。

模型体验

通过 dsh-hooks-claudedsh-hooks-codex 间接影响;它们可以将解析后 hook 输出转为提示词上下文、已阻塞结果或 continuation 反馈。

KV Cache 影响

不会直接失效;请求前缀变更由具名消费方负责。

已知限制与暂缓事项

  • HookOutput.updatedInput 会被解析但不会应用:输入改写是已暂缓的一致性设计问题(见 pre-tool-input-rewrite Agent Note);当 hook 设置它时,桥接会记录 + 警告。完整契约见 src/types.ts