dsh-hooks-claude-code-per-workspace
English | 中文
A DeepSeek Harness (dsh) plugin that runs each workspace's own Claude Code command hooks.
The official @deepseek-ai/dsh-hooks-claude-code bridge reads one process-level configPath once at load (TODO(per-session-hook-config) upstream). This plugin closes that gap for the multi-workspace web surface: at every hook interception point it resolves the current session's own cwd, loads that workspace's .claude/settings.json (plus .claude/settings.local.json, concatenated in order), cached by mtime:size — edit the file and the next hook point picks it up, no reload or restart. When a single dsh web instance has several workspaces open, each workspace's project-level hooks stay independent.
Event → extension-point mapping, stdin payloads, and decision semantics are a faithful port of the official bridge; execution, decoding, and merging come from @deepseek-ai/dsh-hook-protocol.
Install
# from the registry (prebuilt, no build permission needed)
dsh plugin --profile web add dsh-hooks-claude-code-per-workspace
# or equivalently
dshpm install dsh-hooks-claude-code-per-workspace --profile web
# from GitHub (sources; needs the pnpm allowBuilds allowance — see the dsh publish docs)
dsh plugin --profile web add github:chendefine/dsh-hooks-claude-code-per-workspace
You can also install it from the Web GUI's plugin manager. Restart the profile afterwards (bundle membership changes load at boot).
See the 中文文档 below for configuration, the workspace hook format, the trust model, and a comparison with the official bridge.
中文文档
每个工作区(session)运行自己的 Claude Code command hooks 的 DeepSeek Harness 插件。
官方 @deepseek-ai/dsh-hooks-claude-code bridge 在进程加载时读取一个 configPath(进程级、读一次,TODO(per-session-hook-config))。本插件补上这一层:在每个 hook 拦截点,取当前 session 自己的 cwd,加载该目录下的 .claude/settings.json(以及 .claude/settings.local.json,两组按序合并),按 mtime:size 缓存 —— 编辑文件后下一个 hook 点即生效,无需重载或重启。
单个 dsh web 实例打开多个工作区时,每个工作区各自的项目级 hooks 互不干扰。事件 → 扩展点映射、stdin payload、决策语义均为官方 bridge 的忠实移植;执行/解码/合并来自 @deepseek-ai/dsh-hook-protocol。
安装
# 从 npm 安装(预构建产物,无需构建许可)
dsh plugin --profile web add dsh-hooks-claude-code-per-workspace
# 或等价的 dshpm 用法
dshpm install dsh-hooks-claude-code-per-workspace --profile web
# 从 GitHub 安装(源码;需在 profile 的 pnpm-workspace.yaml 里 allowBuilds,见 dsh 发布文档)
dsh plugin --profile web add github:chendefine/dsh-hooks-claude-code-per-workspace
也可以在 Web GUI 的插件管理器中直接安装。安装后重启该 profile(bundle 成员变化需要重启加载)。
配置(全部可选)
profile 的 cordis.patch.yml 中按 id patch 该行(插件不导出 schema,config 原样传入,缺省键在插件内兜底):
- id: hooks-claude-code-per-workspace
config:
settingsPaths: ['.claude/settings.json', '.claude/settings.local.json']
defaultTimeoutMs: 600000 # hook 未写 timeout 时的默认超时(ms)
stderrSummaryMaxChars: 500 # hook/result 事件里 stderr 摘要的长度上限
allowDirs: [] # 目录前缀白名单;空 = 所有工作区都允许
settingsPaths:相对每个工作区 cwd 解析(绝对路径原样使用);多个文件的 hook 组按序拼接 —— 与 Claude Code 项目级两份文件的行为对齐。也可指向.claude/hooks.json(裸事件映射格式同样接受)。allowDirs:非空时,只有 cwd 等于或位于这些目录之下的 session 才加载 hooks;其余静默跳过。
工作区里怎么写 hooks
与 Claude Code 完全同格式(.claude/settings.json 的 hooks 键,其余键被忽略):
{
"hooks": {
"PreToolUse": [
{ "matcher": "bash", "hooks": [{ "type": "command", "command": "./scripts/guard.sh" }] }
],
"Stop": [
{ "hooks": [{ "type": "command", "command": "notify-done.sh", "timeout": 10 }] }
]
}
}
- hook 命令在该工作区目录里执行;
CLAUDE_PROJECT_DIR指向它;命令串里的${CLAUDE_PROJECT_DIR}会被替换。 - 支持 7 个事件:
SessionStart、UserPromptSubmit、PreToolUse、PostToolUse、Stop、SubagentStart、SubagentStop。其余 CC 事件的配置被忽略;仅type: "command"执行(http/mcp_tool/prompt/agent跳过并告警)。 - 输出契约与 CC 一致:exit 2 = 阻断(stderr 为原因);exit 0 + stdout JSON 的
hookSpecificOutput.permissionDecision(deny/ask)与additionalContext生效。 - 每次 hook 运行写入
hook/invoked/hook/result会话事件(dialect 标记claude-code),可在会话日志中审计。
事件 → 扩展点映射
| CC hook | Harness 扩展点 | 语义 |
|---|---|---|
SessionStart |
agent/session-start |
additionalContext → 注入新会话(不可阻断) |
UserPromptSubmit |
agent/pre-step |
deny → 拒绝本轮;additionalContext → 追加到下游 enter 决策 |
PreToolUse |
tools/pre-execute |
deny → 拒绝工具;ask → 转人工确认 |
PostToolUse |
tools/post-execute |
deny → block 并回传原因;additionalContext → 附加到下游决策 |
Stop |
agent/turn-stopping |
阻断 → steer() 原因,强制再走一步 |
SubagentStart |
subagent/start |
additionalContext → 注入子代理 |
SubagentStop |
subagent/end |
仅观察 |
容错
- 工作区没有 settings 文件 → 零 hook(正常,静默)。
- JSON 损坏 / matcher 非法 regex → 该文件告警一次并被忽略,绝不拖垮会话。
- hook 执行失败(无法 spawn 等)→ 非阻断错误,轮次继续。
与官方 bridge 的差异
官方 dsh-hooks-claude-code |
本插件 | |
|---|---|---|
| 配置来源 | 单个 configPath,加载时读一次,进程级 |
每个工作区 cwd 下的 settings 文件,逐点解析 |
| 生效范围 | 整个进程所有 session | 仅该工作区的 session |
| 配置热更新 | 无 | 改文件即生效(mtime+size 缓存) |
configPath/pluginRoot/projectDir 配置 |
有 | 无(pluginRoot 不替换;projectDir 恒为工作区 cwd) |
限制(事件子集、字段缺口、Stop 无限续步、updatedInput 不生效) |
— | 完全相同,见官方 bridge README |
信任模型(必读)
打开一个工作区 = 允许该工作区 settings.json 里声明的任意 shell 命令在 hook 点执行。默认放行所有工作区,每个工作区首次激活会打一条 info 日志(含 hook 数量)。如需收紧,用 allowDirs 白名单。
开发
node test/smoke.mjs # 35 项断言的离线冒烟测试(伪造 ctx/shell/session)
vendor/是@deepseek-ai/dsh-hook-protocol(MIT)逐文件移植:发布版 npm 包不在 harness 安装闭包内,无法作为依赖装进 profile,故内联保持自包含。唯一外部运行时依赖是@deepseek-ai/dsh-llm(peerDependency,由$DSH_HOME/profiles/node_modules平铺兜底解析);本地跑测试可ln -s <dsh 源码>/packages/llm/llm node_modules/@deepseek-ai/dsh-llm。- 真实环境 E2E(已在开发中验证):一次性
DSH_HOME+ headless profile,工作区放 exit-2 的UserPromptSubmithook → 会话日志出现hook/invoked/hook/result(decision=block, exitCode=2),turn 以blocked结束;无配置目录的对照会话零 hook 触发。
卸载
dshpm remove dsh-hooks-claude-code-per-workspace --profile web
# 或
dsh plugin --profile web remove dsh-hooks-claude-code-per-workspace
许可
MIT — 见 LICENSE。vendor/ 内联自 @deepseek-ai/dsh-hook-protocol(MIT,DeepSeek Harness),文件头保留上游署名。
No comments yet. Be the first to write one.