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 dshTarget declaration. 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 version you target in your README and package metadata:
{
"name": "dsh-my-plugin",
"type": "module",
"keywords": ["dsh-plugin", "deepseek-harness"],
"dsh": { "target": "rc.6" }
}
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 dshTarget?
Whenever you adopt a new runtime capability. Declaring a newer target than you actually need shuts out users on older releases.
Next steps
Read the Cordis plugin lifecycle to understand disposal and ordering, then publish and submit your plugin to a directory.