DeepSeek Harness plugin: how to write capabilities with ctx
How to write a DeepSeek Harness plugin comes down to one sentence: export an apply function from a TypeScript module and register every capability through ctx inside it — you never import framework internals, because ctx is the only entry point. The same shape applies to every DSH plugin.
DSH plugin mental model: module + apply + ctx
A DSH plugin is not a base class you extend; it is a plain module that exports apply. The framework calls apply on load and passes a ctx context object where you register capabilities (source):
import type { Context } from '@deepseek-ai/cordis'
export const name = 'my-plugin'
export function apply(ctx: Context) {
// Register capabilities here.
}
That snippet is the complete configuration — nothing else is required. Remember three roles: name is the unique identifier, apply is the load entry, and ctx is the only entry point to framework capabilities. For the fuller decision of function versus object versus class form, see the development guide.
DSH plugin snippet 1: the smallest runnable skeleton
Prove the plugin loads with a single log line before adding anything. Create src/my-plugin.ts:
import type { Context } from '@deepseek-ai/cordis'
export const name = 'hello-plugin'
export function apply(ctx: Context) {
console.log('[hello-plugin] plugin loaded!')
}
If that line prints at startup, your module export and the load path both work. Always write code in the order "make apply run → then register capabilities", otherwise a failure cannot be attributed to loading versus capability setup.
DSH plugin snippet 2: register a tool
A capability for the model is a tool, and inject plus defineTool are both required. Declare the dependency, then register:
import { defineTool } from '@deepseek-ai/dsh-tools'
import type { Context } from '@deepseek-ai/cordis'
export const name = 'my-tool-plugin'
export const inject = ['tools']
export function apply(ctx: Context) {
// ctx.tools is ready here.
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()
},
}))
}
description is written for the model, not for humans — it is how the model decides when to call the tool. For parameter types, output rendering, and error handling, see how to write a tool plugin.
DSH plugin snippet 3: let the plugin accept config
Declare configuration with a Config type plus a same-named Schemastery schema, defaults included, then take a second argument in apply:
import Schema from '@deepseek-ai/schemastery'
import type { Context } from '@deepseek-ai/cordis'
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)
}
The framework validates configuration against the schema and fills defaults, so config inside apply is always complete. How config maps to UI rendering is covered in plugin configuration.
DSH plugin snippet 4: listen to events and clean up manually
Register listeners with ctx.on, and give manual resources a disposer via ctx.effect(). Everything registered through ctx is cleaned up automatically, but timers and connections need an explicit disposer:
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)
})
}
Events have several trigger modes (broadcast, short-circuit, ordered, pipeline), and a pipeline event only continues if you call next(). Choosing the wrong mode shows up as "the logic is written but never runs" — see the events section of the development guide.
Self-checking a DSH plugin: three mistakes and a --patch verification
Self-check these three first; they catch most load failures and resource leaks.
- Mixing export styles — function form uses a named export (
export const nameplusexport function apply), while object and class form use a default export. Mixing them makes the loader unable to identify the form. - Using
ctx.toolswithoutinject— the framework only guarantees a dependency is ready after you declare it; omitting it can yieldundefinedunder some startup orders. - Forgetting
ctx.effect()for manual resources — asetInterval, a connection pool, or a file handle keeps running after the plugin unloads.
These are hard checklist items in the development spec; the full list is in the DSH plugin development spec.
Then run it once to confirm it loads. The lightest local method is a --patch overlay in three steps:
- Capture the absolute path — run
pwdin the plugin project root. Expect: the absolute path that thenamefield below needs. - Write the overlay config — create a
cordis.ymlwhoseinsertsection pointsnameat the plugin source:
- insert:
- id: hello
name: '/absolute/path/to/scratch-plugin/src/my-plugin.ts'
- Start and watch the log — run
pnpm dsh web --patch ./scratch-plugin/cordis.yml. Expect: theconsole.logfrom your plugin prints, which means the module export and the load path both work.
That only proves "the code is written right," not "it can be distributed" — to verify distribution, install into a profile following the last three steps of the development tutorial, then check the installed list in DSH Plugin Hub to confirm the plugin appears and its config renders.
FAQ
Writing a DeepSeek Harness plugin reduces to: **export an apply function from a TypeScript module and register every capability through ctx inside it.** The framework calls apply when loading the plugin and passes the context object, so you never import framework internals and never maintain a registry (source: official "Your first plugin").
**Write both in a DSH plugin — they do different jobs**: name is the plugin's unique identifier and apply is the load entry. The official minimal plugin exports export const name together with export function apply(ctx), and apply accepts a second config argument when you use a Config schema. Running without name works, but logs and dependency resolution become ambiguous (source: official "Your first plugin").
**A DSH plugin registers a model-callable tool in three steps**: declare export const inject = ['tools'], then ctx.tools.register(defineTool({ ... })) inside apply, and finally declare parameters plus output with a render function. **inject is not optional** — without it the framework does not guarantee ctx.tools is ready (source: official "Tools").
**The three most common DSH plugin mistakes are**: ① mixing named and default exports (function form uses a named export, object and class form use a default export); ② using ctx.tools without declaring inject; ③ forgetting ctx.effect() for manual resources so timers and connections leak on unload. The first two break loading or ordering, the third leaks resources (source: official "Your first plugin").
**Verify a DSH plugin by running it once behind a --patch overlay and checking that it loads.** Put the plugin's absolute path in the insert section of a cordis.yml, start with pnpm dsh web --patch ./scratch-plugin/cordis.yml, and a console.log confirms the load timing. Then run the development spec checklist over exports, dependencies, cleanup, and config (source: official "Your first plugin").
Related Terms
- apply
- apply is the entry function of a DSH plugin, usually apply(ctx, config). The framework calls it when loading the plugin, and the plugin registers tools, services, and events through ctx.— https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/develop/basic/index.md
- ctx (context object)
- ctx is the context object the DSH plugin framework passes to apply, and the only entry point to framework capabilities: ctx.tools registers tools, ctx.on registers events, ctx.effect registers disposers, and ctx.get reads other services.— https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/develop/framework/service.md
- defineTool
- defineTool is the DSH plugin helper that declares a tool capability. It takes name, description, parameters, output, and execute, and returns a tool definition you pass to ctx.tools.register.— https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/develop/basic/tool.md
- Config schema
- Config is how a DSH plugin declares its configuration types and validation rules, typically as a Schemastery schema with defaults. The framework validates configuration against it and fills defaults when loading the plugin.— https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/develop/basic/config.md
Sources
- DeepSeek Harness docs - Your first plugin· deepseek-ai
- DeepSeek Harness docs - Tools (defineTool)· deepseek-ai
- DeepSeek Harness docs - Plugin configuration (Config schema)· deepseek-ai
- DeepSeek Harness docs - Services and dependencies (inject)· deepseek-ai