DeepSeek Harness Plugins Guide
dsh has no privileged core to patch — everything is a replaceable config layer. Understand these five things and the rest follows.
What a plugin is
dsh has no privileged core to patch. Model adapters, tools, the session log, even the agent loop itself are plugins — so every one of them is replaceable from config.
A plugin is a module that exports an apply function. The framework calls it at load time with a ctx (context) through which you register capabilities.
import type { Context } from '@deepseek-ai/cordis'
export const name = 'hello-plugin'
export const inject = ['tools'] // 依赖的服务就绪后才会调用 apply
export function apply(ctx: Context) {
ctx.tools.register(/* ... */)
}ctx is a container of services. Each service occupies a stable key — ctx.tools, ctx.llm, ctx.sessions, ctx.agents — and other plugins look services up by key instead of importing an implementation. That is why swapping one provider changes the whole product.
Registration is reversible
Everything registered through ctx — listeners, tools, timers — is cleaned up when the plugin unloads; you never write removeListener. For resources that need manual teardown, return a disposer from ctx.effect().
export function apply(ctx: Context) {
ctx.effect(() => {
const timer = setInterval(() => console.log('heartbeat'), 5000)
return () => clearInterval(timer) // 插件卸载时执行
})
}This property is the foundation of the architecture: because no registration is permanent, plugins mount and unmount at runtime — the agent can even write a plugin mid-conversation and load it into itself.
Installing one
Plugins install into a profile, not into dsh. Once you know what a profile is, installing and removing are each one command.
A profile is a directory under $DSH_HOME/profiles/<name> describing one bootable assembly: which bundles it stacks, the plugins it has installed, and your own cordis.patch.yml. dsh ships web and headless as templates.
Get it running
npx @deepseek-ai/dsh web # Web UI → http://127.0.0.1:3080Requires Node ^22.19 or >=24. Below that, dsh will not start.
Install, inspect, remove
dsh plugin --profile web add dsh-hello-plugin
# 装完先别启动,看看它往配置树里插了什么:
dsh --profile web --dump-config
dsh plugin --profile web remove dsh-hello-plugindsh plugin forwards its arguments to pnpm inside the profile directory, so every pnpm subcommand works (add, remove, update, list). Afterwards it checks whether the package declares dsh.bundle — if so it joins the profile layer list, if not it stays a plain dependency and you get a warning.
--dump-config is your most important diagnostic. It prints the actually-composed config tree; if your plugin is not in it, the problem is the install, not the plugin code.
Layers and overrides
A running dsh is a plugin tree composed of patch layers applied in order. Understand the order and you can change any row — including rows inside someone else’s plugin.
Layers are applied over an empty root in this order:
- 1each bundle patch listed in the profile's dsh.profile.bundles, in list order — @deepseek-ai/dsh-base is always first
- 2the profile's own cordis.patch.yml
- 3$DSH_HOME/cordis.patch.yml, machine-local preferences shared across profiles
- 4each --patch <path> overlay, in argv order
A patch addresses a row by id and replaces it. Later layers win, row by row.
A patch replaces the target row's entire config value; it does not deep-merge keys. To change one field you must restate every key that row needs. This is the most common way people get bitten.
# ~/.dsh/profiles/web/cordis.patch.yml
# 覆盖默认模型(必须重述整行 config)
- replace:
- id: agent-default-model
name: '@deepseek-ai/dsh-agent-default-model'
config:
provider: deepseek-official
model: deepseek-v4-flashTwo consequences. As a plugin author: your patch can override earlier rows by id, restating them in full — and expect users to override you from their own layer, so ship defaults most people will keep. As a user: you can change any row without touching anyone else’s package.
A scratch layer while developing
dsh web --patch ./my-plugin/cordis.ymlPlugin paths in a scratch overlay must be absolute. A patch file contributes configuration only; it does not change the directory the loader resolves modules against.
What to ask before installing
A plugin runs inside the dsh process with everything dsh can reach. The ecosystem is days old — a minute of looking is worth it.
From npm versus from GitHub
An npm install fetches artifacts the author built at publish time; nothing from the package executes during install. A git install fetches source, and pnpm must run the package’s prepare script to produce something loadable.
pnpm 10 refuses to run a git dependency’s build scripts by default, so the first add fails. Authorizing it means writing allowBuilds: { package: true } into that profile’s pnpm-workspace.yaml — which literally means letting that package’s code execute on your machine at install time, outside any sandbox the agent uses. Do it only for source you trust, and pin the commit (github:you/plugin#<sha>) so later pushes cannot silently change what runs.
Reading the catalogue checks
- 1patch-shipped fails: the package declares a config layer whose file never shipped — loading will fail.
- 2rows-resolvable fails: a patch row points at an absolute path on the author's machine. Copied from the tutorial and published as-is; broken on arrival.
- 3client-half-shipped fails: a declared browser half whose ./client export is missing. The nastiest one — it installs and boots cleanly and the UI simply never appears.
- 4prebuilt fails: source only, so installing runs a build script (see above).
- 5Stale version: the declared range excludes the currently published dsh version.
These checks read packaging correctness, not code safety, and not whether the plugin is any good. They rule out dead-on-arrival; they do not tell you whether something is worth installing.
Write one and publish it
From one file to a package anyone can dsh plugin add is a manifest and a patch file.
Three files
hello-plugin/
├── package.json # 声明 dsh.bundle
├── cordis.patch.yml # profile 列入这个包时应用的层
└── index.js # patch 行引用的插件模块{
"name": "dsh-hello-plugin",
"version": "0.1.0",
"type": "module",
"main": "index.js",
"files": ["index.js", "cordis.patch.yml"],
"keywords": ["dsh-plugin", "deepseek-harness"],
"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }
}# cordis.patch.yml — 按包名引用,不是相对路径
- insert:
- id: hello
name: dsh-hello-pluginThe files array must include cordis.patch.yml. Leaving it out is the most common failure in this catalogue: the package installs and the layer simply is not there.
Publish
pnpm publish # 发布时构建好产物,用户安装无需任何构建授权
dsh plugin --profile web add dsh-hello-pluginPublishing to npm instead of asking users to install from git means nobody has to enable allowBuilds. A TypeScript package should either build lib/ before publishing or ship a self-contained prepare script — but the latter pushes the authorization burden onto every user.
Make it findable
- 1add dsh-plugin to keywords in package.json — that is what this catalogue crawls
- 2add the dsh-plugin topic to the GitHub repository
- 3fill in description, license, repository — the catalogue scores these, and they are all a reader has to go on
Bilingual copy goes in dsh.plugin.summary (en and zh-CN keys) with dsh.plugin.displayName for the shown name. The convention is not official yet — 7 of 719 plugins use it — and the ones that do present far better here.
Next: install one
719 installable plugins, 688 passing all seven structural checks. Every entry shows what it mounts, whether its dependency range still resolves, and whether installing runs code on your machine.