DeepSeek Harness plugin development tutorial: end to end
A DSH plugin development tutorial runs through six steps: prepare the environment → create the plugin project → write apply → mount it with a cordis.yml patch → bundle it → install it into a profile and verify. The first three get the plugin running locally; the last three turn it into a package other people can install. The commands and expected output below follow the official docs in order, and every DeepSeek Harness plugin follows the same six-step path.
DSH plugin development step 1: prepare the environment
DSH plugin development needs a working run-from-source DeepSeek Harness, and the official tutorial starts from a repository checkout. With Node and pnpm in place, clone and install (Source):
git clone https://github.com/deepseek-ai/deepseek-harness.git
cd deepseek-harness
pnpm install
If the host itself is not set up yet, get Node versions and pnpm configuration right first via DSH plugin development environment - a version mismatch will manufacture fake failures at every later step.
DSH plugin development step 2: create the plugin project
Create a scratch directory inside the repository root to hold plugin source; tmp/ and scratch-* stay out of version control. The official tutorial does:
mkdir -p scratch-plugin/src
The name scratch-plugin is only an example. One rule actually matters: module paths resolve from the profile directory, so referencing source by absolute path from inside the checkout is the least painful option.
DSH plugin development step 3: write apply so the plugin can load
A DSH plugin at minimum is a TypeScript module that exports apply. Create scratch-plugin/src/my-plugin.ts:
import type { Context } from '@deepseek-ai/cordis'
export const name = 'hello-plugin'
export function apply(ctx: Context) {
// Required dependencies are ready before apply runs.
console.log('[hello-plugin] plugin loaded!')
}
name is the identity, apply is the entry point, and ctx is the channel through which you register capabilities. For the full rules on exports and naming see DSH plugin development spec.
DSH plugin development step 4: mount it with a cordis.yml patch
Use a --patch overlay to insert the local plugin into the config tree, then start the Web UI and watch the output. Three steps (Source):
- Capture the absolute repository root - run
pwdat the repository root. Expect: the absolute path, which thenamefield in the next step needs.
pwd
- Write the overlay config - create
scratch-plugin/cordis.ymland setnameto the path printed above:
- insert:
- id: hello
name: '/absolute/path/to/deepseek-harness/scratch-plugin/src/my-plugin.ts'
- Start with the patch - run the command below. Expect: the Web UI comes up at
http://127.0.0.1:3080and the terminal prints[hello-plugin] plugin loaded!during startup:
pnpm dsh web --patch ./scratch-plugin/cordis.yml
Two things to remember: the plugin path must be absolute, and a patch file only contributes configuration - it does not change the profile directory from which the loader resolves module paths.
DSH plugin development step 5: bundle it
To let other people install it, package the plugin as a bundle. The layout:
hello-plugin/
├── package.json # declares dsh.bundle
├── cordis.patch.yml # the config layer this bundle contributes
└── index.js # the plugin module referenced by the patch row
The four declarations in package.json:
{
"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" } }
}
cordis.patch.yml references the package by name:
- insert:
- id: hello
name: dsh-hello-plugin
Note the difference: the development cordis.yml points at a source file by absolute path, while the published cordis.patch.yml uses the package name - after installation the package is resolvable from the profile's dependencies.
DSH plugin development step 6: install into a profile and verify
Run add, then --dump-config, then start - none of the three is optional. dsh plugin forwards its arguments to pnpm inside the profile directory (Source):
- Install into the profile - run
dsh plugin --profile demo add ./hello-plugin. Expect: the package lands in the profile dependencies; because it declaresdsh.bundle, DeepSeek Harness appends it todsh.profile.bundles. - Check the config layer - run
dsh --profile demo --dump-config. Expect: your patch row appears in the printed config tree. Skipping this step makes it easy to misread "never mounted" as "the plugin has a bug". - Start and verify - run
dsh --profile demo. Expect: the plugin takes effect in the running plugin tree and its output shows up in the terminal.
To see what finished community plugins look like, install one from DSH Plugin Hub and compare.
Once all six steps pass, these are the three places a DeepSeek Harness plugin tutorial gets stuck:
- A relative plugin path - the
namein step 4'scordis.ymlmust be absolute, or the loader resolves it from the profile directory and finds nothing. - A bundle missing
cordis.patch.yml- if it is not infiles, npm will not ship it, the installer gets no config layer, and the plugin installs without mounting. - Confusing local-directory installs with published packages -
dsh plugin add ./hello-plugininstalls from a local directory, which resolves differently from an npm package; see DSH plugin local directory install.
When the flow works end to end, submit the result for community listing via DSH plugin publishing. If a plugin installs but never activates, work backwards from the Fiber state in DSH plugin not taking effect.
Sources: official "Your first plugin", official Cordis tutorial, dsh CLI README
FAQ
A DSH plugin development tutorial runs through six steps: **prepare the environment, create the plugin project, write apply, mount it with a cordis.yml patch, bundle it, and install it into a profile to verify**. The first three get the plugin running; the last three turn it into something others can install. The official tutorial starts from a repository checkout whose run-from-source path already works (Source: official "Your first plugin").
The official DSH plugin tutorial does start from a repository checkout: clone, pnpm install, then create the plugin directory inside the checkout and load it through a --patch overlay. The reason is that module paths are resolved from the profile directory, so keeping the source inside the checkout and referencing it by absolute path is the least painful way to debug locally. If you only need to validate a standalone package, skip ahead to the bundle step and install it with dsh plugin add (Source: official "Your first plugin").
Mount a local DSH plugin with a --patch overlay: run pwd to capture the absolute repository root, write a cordis.yml that inserts the plugin path into the config tree, then start with pnpm dsh web --patch ./scratch-plugin/cordis.yml. **The plugin path must be absolute**, and a patch file only contributes configuration - it does not change the profile directory from which the loader resolves module paths (Source: official "Your first plugin").
Bundling a DSH plugin requires at least four declarations in package.json: **main pointing at the plugin entry, type: module, files covering the entry plus cordis.patch.yml, and dsh.bundle.patch pointing at that patch file**. Leaving cordis.patch.yml out of files means the installer never receives the config layer, so the plugin installs but is never mounted - the single most common packaging omission (Source: official "Packing and installing plugins").
Verify a DSH plugin install in three commands: **install with dsh plugin --profile demo add ./hello-plugin, dump the resolved config with dsh --profile demo --dump-config, then start with dsh --profile demo and watch the plugin output**. The middle step matters - skipping --dump-config makes it easy to misread "never mounted" as "the plugin code is broken" (Source: dsh CLI README).
Related Terms
- cordis.yml
- cordis.yml is the config tree file of DeepSeek Harness: it lists which plugins exist, how they are configured and how they are grouped. During local development an overlay file can be stacked onto the current profile with --patch to insert a plugin that is not published yet.— https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/cordis-tutorial/index.md
- bundle
- A bundle is the distribution unit of a DSH plugin: package.json declares dsh.bundle.patch pointing at a cordis.patch.yml, and on install that patch is appended to the profile's dsh.profile.bundles.— https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/develop/basic/index.md
- --patch (overlay)
- --patch is a DSH plugin local-debugging flag that stacks an extra config file onto the current profile, used to mount a local plugin into the running plugin tree without publishing a package first.— https://github.com/deepseek-ai/deepseek-harness/blob/master/apps/cli/README.md