How to develop a DSH plugin: from a minimal plugin to bundling and install
A DSH plugin is a TypeScript module that exports an apply function: the framework calls apply with a ctx context at load time, and you register capabilities through it, declare dependencies with inject, and accept config via a Config schema.
What a plugin is
In DSH, a plugin is just a module that exports apply. The framework calls it at load time, handing you a ctx (context object) through which every capability is registered (source):
import type { Context } from '@deepseek-ai/cordis'
export const name = 'my-plugin'
export function apply(ctx: Context) {
// Register capabilities here.
}
That is already a complete minimal plugin: name is its unique id, apply is the entry point, and ctx provides the registration surface.
A minimal runnable plugin
A plugin that logs is three core lines. Create src/my-plugin.ts:
import type { Context } from '@deepseek-ai/cordis'
export const name = 'hello-plugin'
export function apply(ctx: Context) {
console.log('[hello-plugin] plugin loaded!')
}
Mount it into the Web UI with a --patch overlay and the terminal prints [hello-plugin] plugin loaded! at startup (source).
Automatic cleanup
Anything registered through ctx — listeners, tools, timers — is cleaned up automatically when the plugin unloads. For resources that need manual cleanup (like a network connection), use ctx.effect() to tell the framework how:
export function apply(ctx: Context) {
ctx.effect(() => {
const timer = setInterval(() => console.log('heartbeat'), 5000)
// The returned function runs when the plugin unloads.
return () => clearInterval(timer)
})
}
Declaring dependencies
If your plugin uses other services (like tools or llm), export an inject array. The framework waits for those services before loading your plugin:
export const name = 'my-tool-plugin'
export const inject = ['tools']
export function apply(ctx: Context) {
// ctx.tools is ready here.
ctx.tools.register(/* ... */)
}
Accepting configuration
Export a Config type and a Schemastery schema of the same name; defaults live in the schema. The framework validates config against the schema and fills defaults at load time (source):
import Schema from '@deepseek-ai/schemastery'
export interface Config {
greeting: string
maxRetries: number
}
export const Config: Schema<Config> = Schema.object({
greeting: Schema.string().default('Hello'),
maxRetries: Schema.number().default(3),
})
export function apply(ctx: Context, config: Config) {
console.log(config.greeting) // User value or schema default.
}
The convention: any parameter that might differ across deployments must be a config field, not hardcoded; invalid config should fail loudly at load time.
Bundling
A distributable plugin package is a "bundle", declared via dsh.bundle in a package.json. The layout:
hello-plugin/
├── package.json # declares dsh.bundle
├── cordis.patch.yml # the config layer this bundle contributes
└── index.js # plugin modules the patch rows reference
The package.json (source):
{
"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" } }
}
And cordis.patch.yml references the package by name:
- insert:
- id: hello
name: dsh-hello-plugin
Installing into a profile
Once bundled, install it with dsh plugin add. The command forwards to pnpm in the profile directory (source):
dsh plugin --profile demo add ./hello-plugin
Because the package declares dsh.bundle, DSH appends it to dsh.profile.bundles. Verify the layer with dsh --profile demo --dump-config before booting with dsh --profile demo.
The GitHub install pitfall
Git installs pull source, not build output — no step runs your build script. So TypeScript packages need a prepare script that the author ships, to build the publish entry after pnpm installs it; on the user side, the first add is blocked by pnpm's allowBuilds guard, so copy the allow key from the error into the profile's pnpm-workspace.yaml and retry (source). To spare users that approval, publish to npm or ship a pnpm pack tarball instead.
Scaffolding to start fast
Generate a project instead of hand-writing. The community scaffold create-dsh-plugin supports tool / events / webui templates and pins the correct @deepseek-ai/dsh-tools version:
npx create-dsh-plugin my-plugin -t tool
Generated projects ship a package.json, tsconfig.json, a dsh.bundle manifest and a cordis.patch.yml; add --verify to build and install into a temp profile to confirm loading. When done, see How to install a DSH plugin to try the result.
Source: official "Your first plugin", official "Bundle and install", dsh CLI README
FAQ
A TypeScript module that exports an apply function. The framework calls apply with a ctx context at load time, and you register capabilities through ctx.
The fastest path is npx create-dsh-plugin my-plugin -t tool, which scaffolds tool / events / webui templates. To hand-write one, start from a minimal plugin that exports apply.
Export an inject array, e.g. export const inject = ['tools']; the framework waits for those services before loading your plugin.
Declare dsh.bundle.patch in package.json pointing at a cordis.patch.yml, then install it with dsh plugin --profile <name> add <package>.
pnpm blocks build scripts from git dependencies by default. Copy the allow key from the error into the profile's pnpm-workspace.yaml and run add again.
Sources
- DeepSeek Harness docs - Your first plugin· deepseek-harness
- DeepSeek Harness docs - Bundle and install· deepseek-harness
- dsh CLI README· deepseek-ai