dsh-cc-hooks
v0.1.1
Published
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
Maintainers
Readme
dsh-cc-hooks
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'] | 项目根向上发现标记 |
| projectDir | 会话 cwd | CLAUDE_PROJECT_DIR 覆盖值 |
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 | $ARGUMENTS 替换 payload → 单轮 LLM | {"ok": true} / {"ok": false, "reason"} | ok=false 阻断 | 30s |
| agent | 同 prompt → subagent(默认关 background,等前台答案) | 同上 | ok=false 阻断 | 60s |
- 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 原样配置直接生效
与官方桥的差异
| 官方桥(进程级) | 本插件(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覆盖:解析(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 分发集成)。
License
MIT。接线语义镜像自 @deepseek-ai/dsh-hooks-claude-code(官方,随
deepseek-harness 维护);共享协议层 @deepseek-ai/dsh-hook-protocol(BSD-3-Clause
→ 官方 0.1.0-rc.6 线,按官方许可使用)。
