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-workspaceYou 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](./LICENSE)。vendor/ 内联自 @deepseek-ai/dsh-hook-protocol(MIT,DeepSeek Harness),文件头保留上游署名。