DeepSeek Harness plugin: bundle, patch, and profile install
Packaging a DeepSeek Harness plugin means turning the plugin you wrote into an npm package that ships a configuration layer (a bundle): package.json points dsh.bundle.patch at a cordis.patch.yml whose rows reference the module by package name, and after a user runs dsh plugin --profile <name> add, that layer is appended to the profile's dsh.profile.bundles. Every DSH plugin bundle obeys the same set of constraints.
DSH plugin bundle and profile: two concepts, two manifests
Authors produce bundles, users boot profiles, and the two never merge. The official definitions are precise (source):
| Concept | What it is | Manifest key | Question it answers |
|---|---|---|---|
| bundle | An npm package shipping a config layer | dsh.bundle | What does this package contribute? |
| profile | A composition directory under $DSH_HOME/profiles/<name> | dsh.profile | Which bundles compose this setup, in what order? |
"Nothing is both." A bundle is what you author and distribute; a profile is what a user boots with dsh --profile <name>. Never hand-write a profile — dsh plugin creates and maintains it.
The three files of a minimal DSH plugin bundle
An installable bundle is only three files. The official layout:
hello-plugin/
├── package.json # declares dsh.bundle
├── cordis.patch.yml # the layer applied when a profile lists this bundle
└── index.js # plugin modules the patch rows reference
{
"name": "dsh-hello-plugin",
"version": "0.1.0",
"type": "module",
"main": "index.js",
"files": ["index.js", "cordis.patch.yml"],
"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }
}
export const name = 'hello-plugin'
export function apply() {
console.log('[hello-plugin] plugin loaded!')
}
files must include cordis.patch.yml, or the installer never receives the config layer — the plugin installs but is never mounted, the most common packaging omission.
A package without dsh.bundle still installs, but only as a plain dependency and activates no layer, with dsh plugin printing a warning. That shape is for a library imported by plugin packages, not for a plugin users enable.
Writing a DSH plugin's cordis.patch.yml
A packaged patch references the module by package name, not by source path. The official minimal form:
- insert:
- id: hello
name: dsh-hello-plugin
The only difference from local debugging: an overlay writes an absolute path (because the loader resolves module paths from the profile directory), while a bundle writes a package name and lets Node module resolution find the installed code. See local debugging.
Installing a DSH plugin into a profile and verifying
dsh plugin --profile <name> <args...> forwards arguments to pnpm in the profile directory, so every pnpm verb works. From the directory containing hello-plugin (source):
- Install into the profile:
dsh plugin --profile demo add ./hello-plugin. The first use initializes the profile (with@deepseek-ai/dsh-baseas its first bundle), links the checkout, and appends your package todsh.profile.bundles. - Check the layer without booting:
dsh --profile demo --dump-config, which you should expect to show a# == dsh-hello-pluginlayer. - Then boot:
dsh --profile demo, which you should expect to show the plugin's load log and working behavior.
Removing uses the same channel: dsh plugin --profile demo remove dsh-hello-plugin, which removes both the dependency and the layer. The full layer-precedence rules are in local debugging.
DSH plugin layer order and the whole-row replacement trap
Understanding two things prevents most "installed but not in effect" and "my config vanished after an override" cases.
- Layer order: bundle patches (in
dsh.profile.bundlesorder) → the profile's owncordis.patch.yml→ the home-level$DSH_HOME/cordis.patch.yml→--patchoverlays; later layers win. - Whole-row replacement, no deep merge: when a patch overrides a row's
config, it must restate every key that row needs; writing only the changed key drops the rest.
Two packaging takeaways: ① your patch can override earlier rows by id (the official dsh-web-app bundle overrides dsh-base rows this way), but it must be complete; ② users can override your rows from their own profile without touching your package, so prefer defaults users are likely to keep and let the schema carry the rest.
Giving a DSH plugin bundle its own command line (advanced)
When you need custom startup flags, mount a provider plugin — the launcher needs no change. Have the plugin inject = ['cmdlineArgs'], parse its own commander program with parseCmdline from @deepseek-ai/dsh-cmdline, and provide its app-owned service from the action; rows that need those values inject that service and read them with !!js:
- id: my-app
name: '@example/my-app'
inject: [myAppStartup]
config:
port: !!js ctx.myAppStartup.port ?? 8080
On --help the provider publishes no service, so those rows never activate — which is exactly how help output avoids triggering business rows.
Once it installs locally, the next step is picking a distribution channel: publishing to npm is covered in publishing to npm, and submitting for directory listing in publishing to the plugin hub. After installing, check the installed list and config in DSH Plugin Hub. For the code-level hard checks, see the development spec.
FAQ
**In DSH plugin work, a bundle is the npm package you author and distribute, and a profile is the composition directory a user boots.** Both are described by a package.json, but the manifest lives under different dsh keys: a bundle declares dsh.bundle (what does this package contribute?), while a profile declares dsh.profile (which bundles compose this setup, in what order?). **Nothing is both** (source: official "Package and install a plugin").
**A DeepSeek Harness plugin bundle needs three files**: package.json (declaring dsh.bundle.patch), cordis.patch.yml (the layer applied when a profile lists the bundle), and the plugin entry module. The official minimal package.json also carries type: module, main, and files — which must include both the entry and the patch file, or the installer never receives the config layer.
**A DSH plugin's cordis.patch.yml references packages by name, not by source path.** The packaged patch is a YAML array whose rows write something like name: dsh-hello-plugin, letting Node module resolution find the installed code; the absolute-path style used during local debugging applies only to a --patch overlay (source: official "Package and install a plugin").
**A DSH plugin whose package.json omits dsh.bundle still installs, but only as a plain dependency, activating no layer** — dsh plugin prints a warning. That shape suits a library imported by plugin packages rather than a plugin users enable. So omitting dsh.bundle shows up as "installed but not in effect" (source: official "Package and install a plugin").
Because **a DSH plugin patch replaces a row's entire config value rather than deep-merging keys**. Your patch can override earlier rows by id, but it must restate **every key the row needs**; writing only the changed key drops the rest. Likewise, users can override your rows in their profile without touching your package, so prefer defaults users are likely to keep (source: official "Package and install a plugin").
Related Terms
- bundle
- A bundle is the unit a DSH plugin author writes and distributes: an npm package that ships a configuration layer, whose manifest declares the patch file via dsh.bundle to insert or override plugin rows.— https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/develop/basic/publish.md
- profile
- A profile is a directory under $DSH_HOME/profiles/<name> describing one runnable composition, with a dsh.profile manifest listing ordered bundles. dsh plugin creates and maintains it; users never hand-write it.— https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/develop/basic/publish.md
- cordis.patch.yml
- cordis.patch.yml is the configuration layer a DSH plugin bundle ships: a YAML array applied when a profile lists that bundle, with rows referencing plugin modules by package name.— https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/develop/basic/publish.md
- dsh.bundle.patch
- dsh.bundle.patch is the DSH plugin package.json field pointing at the bundle's patch file. It is how dsh tells that a package is a plugin bundle rather than an ordinary dependency.— https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/develop/basic/publish.md
Sources
- DeepSeek Harness docs - Package and install a plugin· deepseek-ai
- dsh CLI README (profiles and layer precedence)· deepseek-ai
- DeepSeek Harness docs - Your first plugin· deepseek-ai