How to develop a DSH plugin: from a minimal plugin to bundling and install

Plugin DevelopmentPublished 2026-08-19Author: DSH-Plugin Hub
DSH pluginDeepSeek Harnessplugin developmentCordisbundle
A DSH plugin is a TypeScript module that exports an apply function, built on Cordis: name it, declare dependencies with inject, register capabilities via ctx, and accept config through a Config schema. Walks from a minimal plugin to bundling and installing with dsh plugin add.

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):

ts
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:

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:

ts
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:

ts
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):

ts
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):

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" } }
}

And cordis.patch.yml references the package by name:

yaml
- 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):

bash
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:

bash
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

What is a DSH plugin?

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.

Where do I start developing a DSH plugin?

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.

How does a DSH plugin declare dependencies on other services?

Export an inject array, e.g. export const inject = ['tools']; the framework waits for those services before loading your plugin.

How do I bundle and install a DSH plugin?

Declare dsh.bundle.patch in package.json pointing at a cordis.patch.yml, then install it with dsh plugin --profile <name> add <package>.

A GitHub install errors with allowBuilds. What now?

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