跳到主要内容

dsh-cc-hooks

已验证

dsh-cc-hooks · v0.3.4 · MIT

Run unmodified Claude Code hooks in DSH with per-session discovery: project .claude/hooks + global ~/.claude/hooks + plugin dirs. All five handler types execute (command/http/mcp_tool/prompt/agent) through @deepseek-ai/dsh-hook-protocol (11/31 wired event

安装

dsh plugin add dsh-cc-hooks

用 dsh --profile default --dump-config 确认 layer 已生效 —— 参见安装指南。

源码

标签

作者

说明文档

dsh-cc-hooks

[!IMPORTANT] Only upgrade to this version when upgrading DSH to 0.1.5-rc.2 or later. It binds to the 0.1.5 host contracts (ToolCallId, snapshotEvents() / eventAt(), ^0.1.5-rc.2 peer ranges) and does not work on older DSH — on a pre-0.1.5 host the dsh-llm import itself fails. If your DSH is still 0.1.0-rc.7–0.1.1-rc.2, do not upgrade — stay on v0.1.x. The two lines are mutually exclusive.

Run unmodified Claude Code hooks (all five handler types) in DeepSeek Harness, with per-session / per-plugin discovery — the gap the official bridge (@deepseek-ai/dsh-hooks-claude-code) leaves open (its configPath is process-level, read once at load; its own source carries the TODO(per-session-hook-config)).

项 值
包名 dsh-cc-hooks
依赖 @deepseek-ai/dsh-hook-protocol(官方共享协议层,peer)、dsh-cc-loader(file: 共享解析层)
事件 11/31:官方 7 映射集 + 批次 A 扩展 —— SessionStart / UserPromptSubmit / PreToolUse / PostToolUse / PostToolUseFailure / Stop / SubagentStart / SubagentStop / SessionEnd / PreCompact / PostCompact
动作类型 全部 5 类执行:command / http / mcp_tool / prompt / agent(批次 B);事件×类型落在官方支持矩阵之外 → 解析即跳过 + 警告,能力缺失(tools/llm 服务、subagent 工具、模型路由)→ 警告 + 非阻断,永不崩溃
宿主 DSH 0.1.0-rc.x(协议 peer 钉 0.1.0-rc.6,npm 无 rc.5 发布)

它做什么

每个会话按 session cwd 发现 hooks 配置并合并执行(CC 语义:多份配置 叠加,mergeHookOutputs 按 deny > ask > allow 最严格折叠):

  1. 项目 <projectRoot>/.claude/hooks/hooks.json(projectRoot 由 cwd 向上按 .git 等标记发现)
  2. 用户 ~/.claude/hooks/hooks.json(enableGlobal 可关)
  3. 每个插件目录 <pluginDir>/hooks/hooks.json(pluginDirs 配置;该文件的 ${CLAUDE_PLUGIN_ROOT} 替换为对应插件根)
  • 只读:不写任何文件,单一事实来源永远是 .claude 原文(与 dsh-cc-loader 生态一致)
  • 每会话发现:agent/session-start 预载 + runPoint 惰性兜底;改 hooks.json 后新会话自然生效(会话内热重载不做,记录)
  • 路径变量:${CLAUDE_PLUGIN_ROOT}(解析期,按文件)、 ${CLAUDE_PROJECT_DIR}(运行期,按会话;默认 session workspace,同时导出 CLAUDE_PROJECT_DIR 环境变量)、${CLAUDE_PLUGIN_DATA} 无 DSH 落点(记录)

⚠️ Windows 宿主:deny 请用结构化 stdout,别依赖 exit 2

DSH 的 shell 执行器(dsh-pwsh-local)以 pwsh -NoProfile -NonInteractive -Command <hook command> 运行 hook,而 pwsh 7 的 -Command 把任何非零 native 退出码折叠成 1。协议只有 exit 2 才是 deny → exit 2 到达时变成 exit 1(非阻断错误)→ hook 静默放行。官方桥 @deepseek-ai/dsh-hooks-claude-code 在 Windows 同样受此 影响(LESSONS 1.21)。

hook 作者在 Windows 上应使用结构化 stdout JSON 通道(CC 官方支持,且 exit 0 不受 pwsh 包装影响):

process.stdout.write(JSON.stringify({
  hookSpecificOutput: {
    hookEventName: 'PreToolUse',          // 必须等于触发事件
    permissionDecision: 'deny',           // allow / deny / ask
    permissionDecisionReason: 'blocked',
  },
}))
process.exit(0)

安装

# 先装共享库,再装插件(本地开发 checkout 需包内 pnpm install + pnpm link ../cc-loader)
dsh plugin --profile <name> add dsh-cc-loader dsh-cc-hooks

本地 patch 挂载(Web profile 热更新,改完重启 GUI):

# ~/.dsh/profiles/web/cordis.patch.yml 追加
- insert:
    - id: cc-hooks
      name: 'file:///C:/Users/<you>/.../dsh-cc-ecosystem/packages/cc-hooks/src/index.js'
      config:
        enableGlobal: true
        # pluginDirs: ['C:/path/to/plugin-root', ...]

配置(Schema)

键 默认 说明
enabled true 总开关
defaultTimeoutMs 600000 未设 timeout 的 hook 默认超时(CC 同值)
stderrSummaryMaxChars 500 hook/result 事件 stderr 摘要上限
pluginDirs [] 插件根列表,扫 <dir>/hooks/hooks.json
enableGlobal true 是否加载 ~/.claude/hooks/hooks.json
globalClaudeDir ~/.claude 用户级目录覆盖(测试用)
homeDir os.homedir() 家目录覆盖(测试用)
projectRootMarkers ['.git', '.claude'] 项目根向上发现标记;.claude 让没有 git 仓库的项目也能定位到根
projectDir 会话 cwd CLAUDE_PROJECT_DIR 覆盖值
sandboxMode '' hook 的沙箱模式。空 = 跟随会话;danger-full-access = 不设防(Claude Code 行为)
shellDialect auto ctx.shell 说的是哪种语言:auto 在 Windows 上探一次 / posix / pwsh

hooks.json 格式速查(CC 官方格式,直传 JSON)

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash|Write",        // 可选;默认匹配所有;多值用 | 分隔
        "hooks": [
          { "type": "command", "command": "node scripts/guard.mjs", "timeout": 10000 }
          // type 还可能是 http / mcp_tool / prompt / agent —— 全部执行(批次 B)
        ]
      }
    ],
    "PostToolUse": [
      { "matcher": "Read", "hooks": [{ "type": "command", "command": "echo $CLAUDE_PROJECT_DIR" }] }
    ]
  }
}

31 个事件中,已接线的 11 个(WIRED_EVENTS)全类型执行;其余 20 个事件名在 JSON 里是普通 key,解析通过但暂不执行(parsed-but-inert)。

批次 B:http / mcp_tool / prompt / agent 执行语义(官方)

type 输入 输出 阻断 默认超时
command payload JSON → stdin exit 0 stdout JSON / exit 2 stderr exit 2 / JSON 决策 600s(UserPromptSubmit 30s)
http payload JSON → POST body 2xx JSON object body 按 command 规则解析 仅 2xx + JSON 决策;状态码不能阻断 600s(UserPromptSubmit 30s)
mcp_tool input 字符串值支持 ${tool_input.x} 替换 工具文本按 exit-0 stdout 规则解析 文本 JSON 决策 600s(UserPromptSubmit 30s)
prompt 会话历史(session.deriveMessages(),与主请求同前缀 → 缓存命中)+ 末尾 user(prompt,无 $ARGUMENTS 时输入 JSON 自动追加,CC 官方)+ JSON-only 输出约束 {"ok": true} / {"ok": false, "reason"} ok=false 阻断 30s
agent 同 prompt → subagent(默认关 background,等前台答案) 同上 ok=false 阻断 60s
  • prompt 输入规则(CC 官方):$ARGUMENTS 是 hook 输入 JSON 的占位符;模板里 没有 $ARGUMENTS 时输入 JSON 自动追加到 prompt 末尾。

  • prompt hook 携带完整会话上下文:消息 = agent.session.deriveMessages() (与主 agent 请求字节一致的前缀 → provider prompt cache 命中,多次 hook 调用接近免费) + 末尾一条 user 消息(hook prompt + 输入数据)。Stop 时机 deriveMessages() 已含本轮最终 assistant 消息,故 prompt 如"回顾本轮操作" 能看到 agent 刚做了什么(改了文件、没写 hot.md…),不再盲猜。

  • 模型路由 = 调用 agent 的 provider/model(DSH 无独立 fast-model 池; hook.model 可覆盖)。LLM 只回 JSON——实现强制追加 JSON-only 契约,模型 不包 prose/围栏亦能解码(代码围栏仍容忍)。

  • http 失败(非 2xx / 非 JSON 体 / 连接失败 / 超时)→ 非阻断错误,继续;headers 值支持 $VAR/${VAR} 插值,仅 allowedEnvVars 白名单内变量被解析,未列入 → 空串

  • mcp_tool 直接调用已注册工具的 ToolDefinition(绕过工具事件管线,避免钩子 递归触发自身);server/tool 未连接或 isError → 非阻断错误。server: "plugin:<plugin>:<server>" 映射到 scoped 名 mcp__plugin_<plugin>_<server>__<tool>

  • prompt/agent 返回非 {ok} JSON → 非阻断错误;模型可包 ```json 代码围栏

  • 能力缺失降级:无 tools/llm 服务、无 subagent 工具、无模型路由(既无 hook.model 也无 agent 模型)→ 警告 + 非阻断,永不崩溃

  • 事件×类型支持矩阵(EVENT_TYPE_SUPPORT,官方):SessionStart/Setup 仅 command/mcp_tool;14 个 observe 事件(SessionEnd/PreCompact/PostCompact/ SubagentStart/Notification/MessageDisplay/DirectoryAdded 等)无 prompt/agent; 其余 11 个事件 5 类全支持。不支持组合 → 解析即跳过 + 警告

批次 A 新增接线

事件 DSH 扩展点 matcher subject 语义
PostToolUseFailure tools/post-execute(result.isError) 工具名 工具失败时触发;deny → block + 反馈,与 PostToolUse 同形
SessionEnd agent/disposed(仅顶层会话) 结束原因 DSH 无原因概念 → 恒报 other;observe-only
PreCompact session 事件流 compaction/start manual / auto 手动 /compact 带 sourceCommandId → manual,自动压缩 → auto;observe-only(压缩已落盘,block 无法兑现)
PostCompact session 事件流 compaction/end manual / auto observe-only

if 字段执行过滤

  • 仅 5 个 tool 事件评估:PreToolUse / PostToolUse / PostToolUseFailure / PermissionRequest / PermissionDenied;其他事件上带 if 的 hook 永不运行(CC 官方)
  • 单条权限规则(无 &&/||/列表);Bash 按任一子命令匹配,含 $()/反引号 内命令;前置 VAR=value 剥离;规则无法解析 → fail-open 运行(CC 官方)
  • matcher 同时匹配 DSH 工具名(bash)与 CC 桶名(Bash),CC 原样配置直接生效

Stop / SubagentStop payload:last_assistant_message

CC 官方 Stop/SubagentStop input 携带 last_assistant_message(agent 本轮回合的 最终回复文本),使 hook 无需解析 transcript 即可判断刚结束的回合做了什么。 本插件从 session 事件流取最后一条 assistant/message 的 text 块(reasoning 块排除)填充该字段;无先前回复时降级为空串。prompt/agent 类型 hook 依赖此 字段:它们的 LLM/子代理是裸调用、无工具可读 transcript,若缺失则无法得知 "本轮是否产生修改",只能默认 {"ok": true} 放行 → 一个应阻止停止的 Stop hook(如"有修改未同步 hot.md")会静默失效。

与官方桥的差异

官方桥(进程级) 本插件(per-session)
加载时读一次 configPath 每会话按 cwd 发现 + 缓存;新会话重读
只读一个文件 合并项目 + 全局 + 各插件 hooks/hooks.json
配置变化需重启进程 新会话自然生效
— 项目级优先,插件最后(CC 作用域语义)

决策映射(PreToolUse deny/ask、PostToolUse block+context、UserPromptSubmit reject、Stop steer、SessionStart/Subagent* 注入)与官方桥逐点一致;批次 A 的 PostToolUseFailure 沿用 PostToolUse 的 block+context 映射,SessionEnd 与 PreCompact/PostCompact 为 observe-only(不注入、不阻断)。

PostToolUseFailure 的宿主差异(非零退出码)

CC 中 Bash/PowerShell 命令非零退出码 = 工具失败 → 触发 PostToolUseFailure; DSH 的 shell 工具把非零退出码渲染成 [exit code: N] 标记、result.isError 仅为基础设施故障(spawn 错误/abort)。为对齐 CC 语义,本插件在 tools/post-execute 里同时检查 canonical value 的 exitCode !== 0,非零即 走 PostToolUseFailure(git status 在非仓库目录、bash -c "exit 1" 都是此类)。

测试

node --test test/hooks-merge.test.mjs test/hooks-integration.mjs test/hooks-matrix.test.mjs test/hooks-batch-a.test.mjs test/hooks-executors.test.mjs test/hooks-stop-payload.test.mjs

覆盖:解析(settings/bare 形态、事件×类型矩阵、非法 matcher 抛错)、 ${CLAUDE_*} 替换、三来源发现(项目/全局/插件)、跨源合并、协议折叠 (deny>ask>allow)、matcher 语义、60% 语法矩阵(465 + 12 特殊)、批次 A(if 过滤语义、PostToolUseFailure 分支、SessionEnd 顶层会话、PreCompact/PostCompact manual/auto)、批次 B(http 本地服务器实测、mcp_tool 直调、prompt/agent {ok} 解码、能力缺失降级、runPoint 分发集成)、Stop/SubagentStop payload (last_assistant_message 携带最终回复文本且不含 reasoning 块;无回复时降级空串)。

License

MIT。接线语义镜像自 @deepseek-ai/dsh-hooks-claude-code(官方,随 deepseek-harness 维护);共享协议层 @deepseek-ai/dsh-hook-protocol(BSD-3-Clause → 官方 0.1.0-rc.6 线,按官方许可使用)。