dsh-seams
Field notes on DeepSeek Harness (dsh) seams: extension points, a shell-only context-injection recipe, and four silent-failure traps
- Stars
- 0
- Language
- JavaScript
- Created
- Sep 17, 2026
- Updated
- Sep 17, 2026
Introduction
dsh-seams
DeepSeek Harness (dsh) 的接缝实地笔记。
dsh 是「一切皆插件」架构,官方把可扩展点叫 capability seams。这份笔记记录三件事:
- 能切进去的地方在哪——路径、waterfall、事件名,以及各自能改什么、不能改什么
- 一套能跑的干预配方——不写 TS、不构建 monorepo,挂个 shell 脚本就能往会话上下文里加内容
- 四个会静默失败的坑——都不报错,只会让你得到错误的结论
验证于 @deepseek-ai/dsh@0.1.5-rc.1 / Node v24.21.0 / WSL2。dsh 处于 developer preview,
README 原话 "THERE WILL BE COMPATIBILITY-BREAKING CHANGES"——遇到对不上的地方以你装的版本为准。
作者:@YeKui7 · MIT License 内容全部来自实机验证;每条结论都能用下面的「复现」小节重跑一遍。
🔴 四个静默失败的坑
不报错、不打印、退出码 0。踩中一个就够浪费半天。
1. Node < 22.16 根本跑不起来,且什么都不说
入口 lib/bin.js 末尾:
if (import.meta.main) await runCli();
import.meta.main 是 Node 22.16+ 的特性。在 Node 20 上它是 undefined → runCli() 永不执行 →
整个 CLI 静默退出 0、不打印任何东西、不报任何错。
$ node --input-type=module -e "console.log(typeof import.meta.main)"
undefined # Node 20 → dsh 完全不可用
boolean # Node 22.16+ → 正常
npm 安装时只有 EBADENGINE 警告(commander@15 要 >=22.12.0、undici@8 要 >=22.19.0),
很容易当成无害提示略过。先查 Node 版本,再怀疑别的。
2. cordis.patch.yml 的条目格式:写错不报错,只是不生效
profile 补丁层只有两种合法形状:
# ① 覆盖已有行
- id: system-prompt
config: { ... }
# ② 追加新行
- insert:
- id: my-plugin
name: '@scope/pkg'
config: { ... }
直接写行的形状(- name: '@scope/pkg')是无效的:不报错,--dump-config 里看不到,插件就是不加载。
官方文档的 "smallest working setup" 小节给的是行的形状,不是补丁条目的形状——照抄会踩。
每次改完补丁都该验证:
dsh --profile <name> --dump-config | grep -A3 '<你的 id>'
3. 会话日志是多帧 zstd,常规解压只出第一帧
会话写在 ~/.dsh/sessions/--<cwd 用 - 连接>--/<session-id>/session.v3.jsonl.zstd,
每步追加一个独立的 zstd 帧(实测一次 3 步会话 16 帧)。
zlib.zstdDecompressSync() 与 zlib.createZstdDecompress() 都只解第一帧——只会拿到一条
session 头事件(约 189 字节),看起来就像「dsh 不落盘」。必须按帧魔数 28 B5 2F FD 切开逐帧解:
node tools/unzstd.mjs ~/.dsh/sessions/--*/session-*/session.v3.jsonl.zstd out.jsonl
4. --json 在发布版里可能还没有
仓库 main 分支的 headless 文档描述了 dsh --profile headless --json(NDJSON 事件流,含
tool_call / tool_result),但 npm 上的 0.1.5-rc.1 没有这个旗标(error: unknown option '--json')。
即便有了也要注意:文档写明「every other string and object key is capped at 8 KiB and flagged
with truncated」——长输出会被截断,只有 final 不截。要无损数据就读会话日志,别依赖 --json。
能切进去的地方
| 接缝 | 能做什么 | 不能做什么 |
|---|---|---|
agent/pre-step | 请求派生前唯一的 waterfall:reject 本步,或替换进入本步的消息 | — |
agent/request | 替换调用配置 | 不能改消息 |
tools/pre-execute | 放行 / 拒绝一次工具调用 | 不能改工具输入(updatedInput 未实现) |
tools/post-execute | 变换工具返回 | — |
tools/result | 只读观察冻结后的最终结果 | 不能改 |
agent/inject() | 往下一个 pre-step 排队投喂内容 | 可能错过已认领批次的请求 |
agent/turn-stopping | 停止前做检查、或再推一步 | — |
agent/request-error | 出错后修状态或要求重试 | — |
⚠️ 自写插件往
agent/pre-step塞消息时:有第三方插件记录过,直接 splice 看起来生效、其实被静默丢弃 (后续 listener 从 payload 重建答案,不报错)。正确写法是ctx.on(..., {prepend: true})且先await next()再 append。
干预配方:不写 TS 也能注入上下文
dsh 把 Claude Code 的 hook 协议 桥接成了扩展点。
包 @deepseek-ai/dsh-hooks-claude-code 已是 @deepseek-ai/dsh 的直接依赖(装 dsh 时就装好了),
挂一个 shell 脚本就能注入,不需要写 TS 插件、不需要构建。
三步(hooks/ 与 examples/ 里是可跑的最小样例):
- 写一份 Claude Code 格式的
hooks.json,把UserPromptSubmit指到一个命令 - 命令往 stdout 打:
只认 JSON,plain stdout 不支持{"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":"<要注入的文本>"}} - 在 profile 的
cordis.patch.yml里insert一行@deepseek-ai/dsh-hooks-claude-code,configPath指到那份 hooks.json
验证注入真的到达模型:注入一句哨兵句,任务写成「复述以某前缀开头的那句话」——
模型若说得出来、而任务文本里并没有这句,就是注入生效。实测模型会逐字复述,
thinking 流里也会出现 There's an injection test embedded。会话日志里有 hook/invoked 与
hook/result 事件可交叉验证。
桥的能力边界(来自包文档):
PreToolUse的additionalContext被忽略;allow不预授权PostToolUse的tool_response被拍平成文本transcript_path恒为空——zstd 压缩的会话日志 hook 脚本读不了- 只跑 shell 形式的命令处理器;
http/mcp_tool/prompt/agent处理器被跳过 - 30 个 Claude Code 事件里有 23 个不支持
可重复运行:沙盒要自己造
dsh 的文件工具真改工作区,而 workspace 没有快照/回滚。最省事的做法是一次性 git 仓库当沙盒, 每轮之间:
git checkout . && git clean -fd
实测重置后文件哈希与提交基线逐字节一致。
环境事实速查
| 项 | 值 |
|---|---|
| 会话日志 | ~/.dsh/sessions/--<cwd 用 - 连接>--/<session-id>/session.v3.jsonl.zstd(多帧 zstd) |
| 正文位置 | assistant/message → .data.message.content[],块类型 reasoning / text / tool-call |
| 事件类型 | session turn/start turn/end step/start step/end user/message system/message assistant/message tool/call tool/result request/header request/context hook/invoked hook/result agent/inbox/spliced session/title* approval/* permission/preset sandbox/mode |
| 模型配置 | request/header 事件里可读到实际生效的 provider / model / maxTokens / reasoningEffort |
| profile 位置 | ~/.dsh/profiles/<name>/(cordis.yml 是空壳,实际改 cordis.patch.yml) |
| 组合后的配置树 | dsh --profile <name> --dump-config |
| 插件生态 | GitHub topic dsh-plugin |
| 工具名 | 小写、集合比 Claude Code 大:read bash edit write glob grep todo_write web_fetch web_search subagent skill workflow … 另有 task / job_* / *_goal 一族 |
按工具名判别「返回里有没有新内容」,别按返回的字节数——读一个小文件的返回可能比一次编辑的确认还短。
复现
npm install # 装 dsh(Node ≥ 22.16)
./hooks/inject.sh # 自测 hook 脚本输出
# 把一个一次性 git 目录当沙盒,cd 进去跑:
npx dsh --profile headless "读 a.txt,把行数写进 b.txt"
# 解会话日志(多帧 zstd)
node tools/unzstd.mjs ~/.dsh/sessions/--*/session-*/session.v3.jsonl.zstd out.jsonl
没验证的
- dsh 的 web / tui / SDK 等其它运行形态
- hooks 桥除
UserPromptSubmit外的事件 - DeepSeek 以外的 provider
- Windows 原生(本机是 WSL2)
- 工具分类表里的语义划分(按工具名推的,没做统计验证)
官方资料
- deepseek-ai/deepseek-harness
docs/agent-lifecycle.md— 回合 / 步生命周期时序图,一张图说清所有 waterfall 的位置docs/subsystems/core.md— 各 waterfall 的完整签名(搜agent/pre-step)docs/capability-seams.md— capability seam 总图packages/hooks/hooks-claude-code/README.md— 桥的能力边界与已知限制packages/bundle/headless/README.md— headless 运行器与--json契约