← Back to home@YeKui7

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/ 里是可跑的最小样例):

  1. 写一份 Claude Code 格式的 hooks.json,把 UserPromptSubmit 指到一个命令
  2. 命令往 stdout 打:
    {"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":"<要注入的文本>"}}
    
    只认 JSON,plain stdout 不支持
  3. 在 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 契约