How DeepSeek-Harness Plugins Work Under the Hood
A developer primer on dsh plugins — the apply(ctx, config) contract, the three ways to write one, the package.json dsh field, and how to get listed.
A DeepSeek-Harness (dsh) plugin is a plain JavaScript or TypeScript module that exports an apply(ctx, config) function. That's the entire contract — there's no separate manifest file describing what a plugin is or does. Distribution metadata lives in a dsh field inside the plugin's package.json, not in an independent schema file. This post walks through that contract, how the three plugin shapes differ, how a plugin gets packaged for dsh plugin add, and how it ends up discoverable.
The contract: apply(ctx, config)
The simplest possible plugin:
import type { Context } from '@deepseek-ai/cordis'
export const name = 'my-plugin'
export function apply(ctx: Context) {
// register capabilities on ctx here
}
ctx is the Cordis context your plugin receives on load. Whatever you register through it — event listeners, ctx.tools.register() calls, ctx.effect() cleanup — is automatically torn down when the plugin unloads. You don't write manual dispose logic for the common cases.
A plugin can optionally export:
inject— an array of service names it depends on (e.g.['tools', 'llm']). The framework waits until those services exist before callingapply, and unloads the plugin automatically if a dependency disappears.Config— a Schemastery schema describing the plugin's user-configurable options, validated and defaulted when dsh loads the plugin.
Three ways to write one
Functional (shown above) is the default and covers most plugins — a single apply function is all you need.
Object-style wraps the same pieces in a default export:
export default {
name: 'my-plugin',
inject: ['tools'],
apply(ctx) {
// ...
},
}
Class-style, extending Service, is for plugins that expose a service of their own for other plugins to depend on:
export default class MyService extends Service {
static inject = ['tools']
constructor(ctx: Context) {
super(ctx, 'myService') // mounts as ctx.myService
}
}
Once mounted, any other plugin can declare inject: ['myService'] and get access to ctx.myService — that's how services compose across plugins written by different authors.
Every dsh feature is just a plugin, differently shaped
This is the part that makes dsh's architecture legible: there is no separate registration system for tools vs. commands vs. skills vs. MCP servers. They're all the same apply(ctx, config) mechanism, aimed at a different extension point:
| Capability | How it's implemented |
|---|---|
| Tool | ctx.tools.register() — its schema flows into prompt assembly automatically |
Command (/xxx) | ctx.commands — UI-only, produces no model message |
| Skill | a registered prompt section plus a tool; the skill content is injected when the tool is invoked |
| MCP server | one plugin per server: discover its tools, then ctx.tools.register() each |
| Hook | a listener on lifecycle extension points like agent/session-start, agent/pre-step, tools/pre-execute |
| LLM adapter | a LlmAdapter subclass registered via ctx.llm.registerAdapter() |
| UI extension | listens on session/event, or registers a ConversationNodeDefinition into the built-in web client |
| Permission / sandboxing | returns allow / deny / ask from tools/pre-execute, or a custom ctx.sandbox backend |
| Background/cron job | registered via ctx.jobs, triggering followup(..., { source: { kind: 'cron' } }) |
| Subagent delegation | registered into the ctx.subagents provider registry |
Practically, this means if you already understand how to write a tool plugin, you're most of the way to understanding how to write a hook, a command, or an MCP bridge — it's the same shape pointed at a different part of ctx.
Packaging: the dsh field in package.json
A plugin's package.json doesn't need a bespoke manifest — it needs a dsh.bundle.patch field:
{
"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" }
}
}
dsh.bundle.patch points at a YAML file — a patch — that describes what this package actually inserts into the running Cordis plugin tree:
- insert:
- id: hello
name: '/absolute/or/module/path/to/my-plugin.ts'
config:
someOption: true
Each entry either inserts new plugin rows (with an id, a name pointing at the module, and optional config), or overrides the config of an existing row by id. Note that a patch replaces the target row's config wholesale rather than deep-merging it — if you're overriding one field, you need to restate the rest.
There's a second, related concept worth knowing: a profile (the thing you pass to dsh --profile <name>) is itself described by a dsh.profile.bundles field — an ordered list of which bundle packages compose that profile, with @deepseek-ai/dsh-base always first. When you run dsh plugin --profile <name> add <package>, dsh installs the package via pnpm and, if it declares dsh.bundle, automatically appends it to that list. You never hand-edit it.
Typical repo layout
Putting the pieces from above together, a real installable plugin repository usually looks like this:
hello-plugin/
├── package.json # declares dsh.bundle.patch
├── cordis.patch.yml # what this bundle inserts/overrides
├── index.js # entry point — exports apply / name / inject / Config
├── src/ # if TypeScript, compiled output goes to lib/
└── README.md
If you distribute source via GitHub rather than a prebuilt npm package, add a prepare script so pnpm compiles src/ to lib/ automatically on install — see our install guide for what happens when that script needs explicit allowBuilds permission from the person installing it, and why that prompt exists.
Getting your plugin discovered
DeepSeek AI doesn't run a plugin marketplace, so there's no submission form on the official side. Two things actually move the needle:
- Tag your repository with the GitHub topic
dsh-plugin. It's the one discovery mechanism the official docs mention by name, and it's what search-based tools and crawlers look for first. - Submit it to the community index.
awesome-dsh-plugin/awesome-dsh-pluginis the most actively maintained community list — currently 365 plugins across 11 categories — and it accepts PRs adding new entries in its established- [owner/repo](url) - one-line descriptionformat.
FindHarness's own /plugins catalog is built from that same community-curated data, enriched with live GitHub metadata (stars, license, last push, full README). Getting listed there means getting into awesome-dsh-plugin and keeping your repo's topic and metadata accurate — there's no separate submission step on our end.
For the install-side view of all of this — what a user actually runs to pull your plugin down, and the security prompt they'll see if it needs a build step — read How to Install DeepSeek-Harness Plugins. And if you want a sense of what "good" looks like before you start, 10 Best DeepSeek-Harness Plugins in 2026 is a real, star-ranked sample of what's shipped.