AllDSH
中文

intermediate · 3 min read · updated 2026-09-06

Build a DSH plugin: from zero to loaded plugin

A practical walkthrough for building a DeepSeek Harness plugin, from package setup to the apply(ctx) entry point and local testing.

Written by Chiyu · Maintainer and editor

Maintains this directory: checks every install command against the published package, assigns verification levels, runs the automated scans, and writes the guides. ·

Building a DSH plugin is closer to writing a small library than to writing an extension for a closed application. You need a package, an entry point, and a harness-compatibility declaration (the dsh.compatibility block, which directories record as dshTarget). This guide walks through all three, then shows how to test the result in a real session.

What a plugin actually is

A plugin is a package that exports an apply(ctx) function. The harness calls it during startup with a context object, and everything you register happens through that object: tools, commands, interface surfaces, event handlers. When the plugin is unloaded, the runtime disposes what you registered.

That design means you never reach into global state. If your plugin can only work by mutating something it does not own, it will break on the next release.

Step 1: create the package

mkdir my-dsh-plugin && cd my-dsh-plugin
npm init -y
npm pkg set type=module
npm pkg set name=dsh-my-plugin

Add the ecosystem topic so directories can discover it later, and declare the harness versions you tested against in your package metadata (and repeat it in your README):

{
  "name": "dsh-my-plugin",
  "type": "module",
  "keywords": ["dsh-plugin", "deepseek-harness"],
  "dsh": {
    "compatibility": { "dsh": ">=0.1.0-rc.6 <0.2.0", "profiles": ["web"] }
  }
}

Step 2: write apply(ctx)

Start with the smallest useful plugin: one tool the model can call.

import type { Context } from '@deepseek-ai/dsh';

export function apply(ctx: Context) {
  ctx.tool({
    name: 'word_count',
    description: 'Count words in a piece of text.',
    parameters: {
      type: 'object',
      properties: { text: { type: 'string', description: 'Text to count' } },
      required: ['text'],
    },
    async execute({ text }: { text: string }) {
      const words = text.trim().split(/\s+/).filter(Boolean).length;
      return { words };
    },
  });
}

Three things matter in that snippet. The description is written for a model, not a human, because it is what the model sees when deciding whether to call the tool. The parameter schema is explicit, because vague schemas produce bad calls. The return value is structured data, not a formatted string, because the caller usually wants to reason over it.

Step 3: register more than tools

Tools are the most common surface, but not the only one. Name the surface you need and register through ctx:

  • Commands, for things you trigger yourself from the composer.
  • Events, for reacting to session lifecycle changes.
  • Interface surfaces, if you are building UI rather than capability.
  • Capability seams, when you are providing an implementation for something another plugin consumes, such as a memory provider.

Step 4: test it in a real session

Install your plugin into the web profile from the directory that contains it:

dsh plugin --profile web add /absolute/path/to/my-dsh-plugin

Then start the harness and check two things: that the plugin loaded without an error, and that the model actually calls your tool when the situation warrants it. A tool that exists but is never chosen usually has a description problem, not a code problem.

Step 5: handle failure honestly

Plugin failures should be legible. If a tool call cannot complete, return an error that names what was missing rather than an empty result. If your plugin needs configuration, fail at load time with a message that says which field is missing, instead of failing on first use three sessions later.

Step 6: document the contract

Before publishing, write down what you depend on: the capabilities you register, the permissions you need, and the harness version you target. That documentation is what a directory needs in order to list you accurately, and it is what a reviewer needs in order to trust the plugin.

FAQ

Do I need TypeScript?

No, but types for the context object prevent most of the mistakes people make in their first plugin.

Can a plugin depend on another plugin?

Indirectly, through capability seams. Direct private imports between plugins break on upgrade and create ordering problems.

How do I test without publishing to npm?

Install from a local path, as shown in step 4. The harness treats a local directory like any other package.

When should I bump the compatibility declaration?

Whenever you adopt a new runtime capability. Declaring a wider range than you actually verified shuts out users on older releases for no benefit.

Next steps

Read the Cordis plugin lifecycle to understand disposal and ordering, then publish and submit your plugin to a directory.

Sources & references

The technical claims on this page rest on the primary documents below. Each link is followed so it can be checked directly.

  1. [1]deepseek-ai/deepseek-harness — GitHubUpstream repository for DeepSeek Harness, the Cordis runtime and the plugin loading model.
  2. [2]@deepseek-ai/dsh on npm — npmPublished harness package, including the release tags our dshTarget field tracks.
  3. [3]About semantic versioning — npm DocsThe versioning convention behind the compatibility and dshTarget guidance.
  4. [4]Keep a Changelog 1.1.0 — Keep a ChangelogThe changelog convention recommended for plugins that want maintainable release notes.

All links last verified 2026-09-11

ESC

Type to search the index. Built at deploy time by Pagefind.