Hooks 桥(Claude / Codex)
一句话版:DSH 的核心扩展面是类型化拦截点(native 插件就够);
hooks-claude-code/hooks-codex是桥插件:把你已有的 Claude Code / Codex 的hooks.jsonshell 钩子,原样跑在 DSH 的这些拦截点上,兼容旧生态。
如果你以前用 Claude Code / Codex 配了一批 shell 钩子(pre-commit、门禁、通知…),不想在 DSH 里重写,dsh-hooks-* 能让它们继续跑。
一、两个概念,别混
| native 插件(推荐) | hooks 桥(兼容) | |
|---|---|---|
| 是什么 | 挂在 DSH 类型化拦截点上的普通 Cordis 插件 | 翻译"外部 shell-hook 协议"到同一拦截点的桥 |
| 体验 | 类型化返回、无序列化边界 | 走子进程 shell 钩子 |
| 何时用 | 新写的扩展 | 已有 Claude/Codex hook 配置想兼容 |
源码原话:native 插件能更强大地做到桥做的一切。桥只作为兼容路径存在;凡是新写的东西,都应该做 native 插件。
二、包
| 包 | 角色 |
|---|---|
dsh-hook-protocol | 共享 shell-hook wire 协议(matcher、stdlib codec、ctx.shell 执行、hook/* 事件) |
dsh-hooks-claude-code | Claude Code 钩子桥(CC 方言:stdin 载荷、${CLAUDE_PLUGIN_ROOT}/${CLAUDE_PROJECT_DIR} 替换) |
dsh-hooks-codex | Codex 钩子桥(Codex 方言) |
hook-protocol 不是插件:不注册、不注入任何东西,只是两个桥共用的库。
三、配置:接一个 Claude hooks.json
# cordis.yml / patch
- id: hooks-claude-code
name: '@deepseek-ai/dsh-hooks-claude-code'
config:
configPath: ./.claude/hooks.json # 必填:hooks.json 或带 hooks 键的 settings
pluginRoot: ./.claude/plugins/my # 选填:替换 ${CLAUDE_PLUGIN_ROOT}
projectDir: . # 选填:替换 ${CLAUDE_PROJECT_DIR};缺省 = session cwd
defaultTimeoutMs: 600_000 # 选填:钩子没设超时时的默认(CC 默认)
stderrSummaryMaxChars: 500 # 选填:hook/result 事件持久化 stderr 摘要的字符上限
Codex 桥配置形状相同,多一个可选 model(打在每条 stdin 载荷上),configPath 通常指向 ./.codex/hooks.json。
- 支持 CC/Codex 支持的 command-hook 子集;映射:钩子中性结果 → harness 类型化 Decisions(allow/deny/ask 等);
hook/*会话事件记录钩子执行 - 配置只在装载时解析一次,
configPath是进程级的(相对路径按启动 cwd 解析,没有按 session 的发现);读取/解析失败隔离(记日志、注册零钩子) - 只有
type: 'command'的 shell 钩子会跑,http/mcp_tool/prompt/agent等 handler 解析后跳过并警告
四、hook 协议形状
两个桥共享一套协议原语,各自只管方言差异:
| 关注点 | 共享库(dsh-hook-protocol) | 桥(-claude / -codex) |
|---|---|---|
| matcher 校验/匹配 | matcherDiagnostic / matchesMatcher | 选 mode:claude = 字面量或正则,codex = 恒为正则 |
| 跑钩子 | runHook:stdin 载荷 + env 经 ctx.shell | 构造每事件 stdin 载荷 + 方言 env |
| 解码/合并 | parseHookOutput → HookOutput;mergeHookOutputs → 最严格结果 | 把中性结果映射成拦截点类型化 Decision |
| 持久记录 | hook/invoked / hook/result 会话事件 | 围绕每次调用调用它们 |
关键语义:
- exit code 2 = 阻塞(带 stderr);其他失败是非阻塞错误。
- 合并取最严格:
deny > ask > allow;continue:false首现即粘住;additionalContext/systemMessages按序累积。 - matcher:
claude模式纯[A-Za-z0-9_|]+当字面量(管道 = 精确交替),其余当正则;codex模式恒为无锚正则。 hook/*事件是 log-only(像compaction/*),非SurfaceEventType:配对hook/invoked与hook/result,stderr 摘要截到stderrSummaryMaxChars。
五、拦截点 → 决策映射
Claude 桥把 CC 钩子映射到 DSH 拦截点:
| CC 钩子 | DSH 拦截点 | 映射 |
|---|---|---|
SessionStart | agent/session-start(emit) | additionalContext → agent.inject() 进新会话 |
UserPromptSubmit | agent/pre-step(waterfall) | deny → reject;仅 additionalContext → next() 委托后附加上下文 |
PreToolUse | tools/pre-execute(waterfall) | deny → deny;ask → ask |
PostToolUse | tools/post-execute(waterfall) | deny → block + feedback |
Stop | agent/turn-stopping(serial) | 阻塞的 Stop 经 steer() 强推下一步 |
SubagentStart | subagent/start(emit) | additionalContext → 注入活的进程内子 agent |
SubagentStop | subagent/end(emit) | 只观察 |
Codex 桥实现 10 个点里的 5 个(PreToolUse/PostToolUse/SessionStart/UserPromptSubmit/Stop),用 block(exit 2)代替 deny,PreToolUse 无 allow/ask。
- emit 点(
SessionStart/SubagentStart/SubagentStop)分离运行,没有拦截点在等;运行链被追踪,dispose 时先中止仍在跑的钩子进程、再 drain 续体。 - matcher 主体:工具名(
PreToolUse/PostToolUse)、会话来源(SessionStart)、常量agent_type=general-purpose(SubagentStart/SubagentStop);UserPromptSubmit/Stop忽略 matcher。 - 同一拦截点上的多个钩子按配置顺序串行执行并折叠最严格。
六、与 tools 门禁的区别
两者最终落在同一些拦截点上,但别混(口径与 监听事件 一致):
| native 门禁(推荐) | hooks 桥 | |
|---|---|---|
| 实现 | ctx.on('tools/pre-execute', ...) 返回 PreToolDecision | 把外部 shell 钩子的 stdout/exit code 翻译成同一决策 |
| 类型 | 类型化返回值,无序列化边界 | 经子进程 + stdin/stdout 序列化 |
| 何时用 | 在 DSH 上配新门禁/拦截 | 已有 Claude/Codex hooks.json 想兼容 |
七、什么时候用
| 场景 | 用 |
|---|---|
| 已有大量 Claude/Codex 钩子,想无缝迁移 | hooks 桥 |
| 在 DSH 上配新门禁/拦截 | native 插件(tools 流水线、agent 事件) |
接 @dsh-external 社区钩子 | 按插件装 |
八、验证
# 看 hooks 桥是否装载
dsh web --dump-config | grep -iE "hook"
# 会话里看 hook 执行
zstdcat ~/.dsh/sessions/*/*/session.jsonl.zstd | grep -E '"hook/' | head