DeepSeek Harness plugin development guide: forms, debug

Plugin DevelopmentPublished 2026-09-12Author: DeepSeek Plugin Market
DSH pluginDeepSeek Harnessplugin development guideCordisservice
DeepSeek Harness (DSH) plugin development guide: choose a plugin form, place a capability in tools, a service, or an event, and debug with a --patch overlay.

This DeepSeek Harness (DSH) plugin development guide is about choosing a route: which of the three plugin forms to use, whether a capability belongs in tools, a service, or an event, and whether to debug with a --patch overlay or a profile install. Settle those three lines first and you will avoid rework when you start coding.

DSH plugin development: three lines to decide

A DSH plugin goes from idea to running by answering three questions in order. They map to three parts of the official docs (source):

  1. Form — is this plugin a function that only registers capabilities, an object that also carries dependencies and config, or a class that provides a service to other plugins?
  2. Capability placement — is the capability for the model (a tool), for other plugins (a service), or just a hook at some moment (an event)?
  3. Debugging mode — use a --patch overlay while editing source, or package and install into a profile to verify?

The next three sections follow that order, and each gives decision criteria instead of an API dump.

Choosing a DeepSeek Harness plugin form: function, object, class

Function form covers the vast majority of plugins; class form is only for providing a service. The official wording is "Function form is sufficient in most cases," pointing to services as the reason to use class form (source).

FormExport styleSignal to choose it
Functionnamed export name + applyYou only register capabilities
Objectexport default { name, inject, apply }You want name / inject / apply in one default-exported object
Classexport default class ... extends ServiceYou provide a service other plugins consume

Here is the minimal class form — the constructor performs synchronous initialization, and the service name (myService here) is what other plugins use in inject:

ts
import { Service, type Context } from '@deepseek-ai/cordis'

export default class MyService extends Service {
  static inject = ['tools']

  constructor(ctx: Context) {
    super(ctx, 'myService')
    // Perform synchronous initialization in the constructor.
  }
}

Named exports and default exports must not be mixed: function form uses a named export, while object and class form use a default export. That is how the loader identifies the plugin form. See the development spec for the full checklist.

Where a DeepSeek Harness plugin capability belongs: tools, service, events

Placement follows the caller, not your preference. A DSH plugin capability has three kinds of consumer, matching three registration styles (source):

  • Model calls it → register a tool with ctx.tools.register, after inject: ['tools'].
  • Other plugins call it → register a service, using class form so consumers reach it through inject.
  • Framework moment → register an event listener with ctx.on to hook plugin load and unload.

Events themselves have several trigger modes, and picking the wrong one means your logic never runs or runs out of order: emit broadcasts, bail short-circuits on the first result, serial runs in order, and waterfall is a pipeline that only continues if you call next(). Confirm the mode before writing a listener.

Declare the tool's parameters and output with defineTool so the model knows how to call it and the framework knows how to render the result:

ts
import { defineTool } from '@deepseek-ai/dsh-tools'

export const inject = ['tools']

export function apply(ctx: Context) {
  ctx.tools.register(defineTool({
    name: 'my_cap',
    description: 'Execute my capability.',
    parameters: {
      input: { type: 'string', required: true },
    },
    output: {
      schema: { type: 'string' },
      render: (_args, value) => [{ type: 'text', text: value }],
    },
    async execute(args) {
      return args.input.toUpperCase()
    },
  }))
}

When a DeepSeek Harness plugin provider must be replaceable: the three-role design

Split packages only when the capability must swap implementations without touching callers — never preemptively. The official design separates three roles and uses Bash execution as the example: dsh-shell (Service Definition) owns the contract, dsh-bash-local (Service Provider) implements local execution, and dsh-tool-bash (Consumer) exposes it as a model-callable tool (source).

The three dependency rules are the heart of it:

  • The Service Provider depends on the Service Definition;
  • the Consumer depends on the Service Definition;
  • the Provider and Consumer do not depend on each other.

Swapping a provider is then a one-line change in cordis.yml, with the Definition and tool untouched:

yaml
# Local execution
- name: '@deepseek-ai/dsh-bash-local'
# Replace this row with another package that provides the same service.

The official design points are worth copying into a review checklist: do not split preemptively (a simple tool plugin does not); Request/Result types belong to the Service Definition; explicit over implicit — resolve defaults in an explicit resolve(request) step rather than hiding ?? default inside run().

Debugging a DSH plugin: --patch overlay versus install verification

Use a --patch overlay while editing source and a profile install to verify the artifact — they verify different things. The official tutorial mounts a local plugin into the Web UI with an overlay in three steps (source):

  1. Get the absolute path — run pwd in the plugin repository root. Expect: the absolute repo root, which the name field below needs.
  2. Write the overlay config — create a cordis.yml that inserts the local plugin into the config tree:
yaml
- insert:
    - id: hello
      name: '/absolute/path/to/deepseek-harness/scratch-plugin/src/my-plugin.ts'
  1. Start with the patch — run the command below. Expect: the Web UI starts with the plugin mounted, and later source edits take effect on restart:
sh
pnpm dsh web --patch ./scratch-plugin/cordis.yml

Two hard rules: the plugin path must be absolute, and a patch file contributes configuration but does not change the profile directory the loader resolves module paths from. So "it is in the patch but never loads" is usually a path-resolution problem, not a syntax problem.

To verify the packaged artifact, switch to a profile install: dsh plugin --profile <name> add <package>, then check the config layer with dsh --profile demo --dump-config before starting. For the trade-offs and more techniques, see local debugging.

From DSH plugin development guide to release

Once the guide settles the route, proceed through tutorial → spec → release. A suggested order:

  1. Run the six steps of the development tutorial to get a first installable plugin;
  2. run the development spec checklist over exports, dependencies, cleanup, and config;
  3. package and distribute via packaging into a bundle and publishing to the plugin hub;
  4. confirm the plugin shows up in the installed list of DSH Plugin Hub, with config rendering correctly.

If the plugin loads but behaves wrong (no effect, or a service registration error), start with plugin not activating and service already registered. Most cases come down to a wrong form or a missing inject.

FAQ

In a DeepSeek Harness plugin development guide, do I pick the form before writing code?

Pick the form first in DeepSeek Harness plugin development. The decision order is: **register capabilities only → function form; bundle inject and Config together → object form; expose a service other plugins consume → class form.** The official docs state that function form is sufficient in most cases, and class form is for plugins that provide a service (source: official "Your first plugin").

Should a DeepSeek Harness plugin capability be a tool, a service, or an event?

In a DeepSeek Harness plugin, decide by who calls it: register a **tool** (ctx.tools.register) when the model calls it, a **service** when other plugins call it, and an **event listener** when you only need to run logic at a framework moment. One plugin can do several at once — declare inject first, then register each kind in apply (source: official "Three-role capability design").

When should a DSH plugin be split into three packages?

A DSH plugin should be split into three packages only when the capability needs a **replaceable provider**. The three roles are Service Definition (contract plus Request/Result types), Service Provider (implementation), and Consumer (model-callable tool). The first official design point is: do not split preemptively — a simple tool plugin does not need it (source: official "Three-role capability design").

Should I debug a DeepSeek Harness plugin with --patch or a profile install?

Debug a DeepSeek Harness plugin by what you are verifying: use a **--patch overlay** to check whether the plugin loads at all, since a source edit plus restart is enough; use dsh plugin --profile <name> add only to verify the packaged artifact installs. A patch contributes configuration but does not change the profile directory the loader resolves module paths from, so local plugin paths must be absolute (source: official "Your first plugin").

How does this guide differ from the DeepSeek Harness plugin spec and tutorial?

The DeepSeek Harness plugin development guide, spec, and tutorial answer different questions: the **guide** answers "which route to take" (form, capability placement, debugging mode); the development spec answers "is the code correct" (exports, naming, cleanup, checklist); the tutorial answers "how to get it running step by step." Read the guide to pick a plan, follow the tutorial to build, then run the spec checklist.

Related Terms

Service Definition / Provider / Consumer
DSH plugins split a capability that needs a replaceable provider into three roles: the Service Definition owns the service and its Request/Result types, the Service Provider implements the behavior, and the Consumer exposes it as a model-callable tool. Only the three roles together form the complete seam; no single role is a seam.https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/develop/practice/index.md
apply
apply is the entry function of a DSH plugin. The framework calls it when loading the plugin and passes a ctx context object through which the plugin registers tools, services, and events.https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/develop/basic/index.md
inject
inject declares the services a DSH plugin depends on, such as ['tools', 'llm']. The framework waits for all of them to be ready before loading the plugin, so apply can use ctx.tools directly.https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/develop/framework/service.md
--patch overlay
A `--patch` overlay is a DSH plugin local-debugging switch that lets dsh load an extra cordis.yml on top of the current profile to insert a local, unpublished plugin. It contributes configuration only and does not change the profile directory used to resolve module paths.https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/develop/basic/index.md

Sources