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.2or later. It binds to the 0.1.5 host contracts (ToolCallId,snapshotEvents()/eventAt(),^0.1.5-rc.2peer ranges) and does not work on older DSH — on a pre-0.1.5 host thedsh-llmimport itself fails. If your DSH is still0.1.0-rc.7–0.1.1-rc.2, do not upgrade — stay onv0.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 最严格折叠):
- 项目
<projectRoot>/.claude/hooks/hooks.json(projectRoot由 cwd 向上按.git等标记发现) - 用户
~/.claude/hooks/hooks.json(enableGlobal可关) - 每个插件目录
<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 线,按官方许可使用)。