DeepSeek Harness plugin: how to write capabilities with ctx

Plugin DevelopmentPublished 2026-09-12Author: DeepSeek Plugin Market
DSH pluginDeepSeek Harnesshow to write a pluginapplyCordis
How to write a DeepSeek Harness (DSH) plugin: apply and ctx, a tool, a config, an inject, and an event — plus the three mistakes that break loading.

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):

ts
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:

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:

ts
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:

ts
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:

ts
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.

  1. Mixing export styles — function form uses a named export (export const name plus export function apply), while object and class form use a default export. Mixing them makes the loader unable to identify the form.
  2. Using ctx.tools without inject — the framework only guarantees a dependency is ready after you declare it; omitting it can yield undefined under some startup orders.
  3. Forgetting ctx.effect() for manual resources — a setInterval, 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:

  1. Capture the absolute path — run pwd in the plugin project root. Expect: the absolute path that the name field below needs.
  2. Write the overlay config — create a cordis.yml whose insert section points name at the plugin source:
yaml
- insert:
    - id: hello
      name: '/absolute/path/to/scratch-plugin/src/my-plugin.ts'
  1. Start and watch the log — run pnpm dsh web --patch ./scratch-plugin/cordis.yml. Expect: the console.log from 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

How do you write a DeepSeek Harness plugin in one sentence?

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").

Are both name and apply required when writing a DSH 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").

How do you write a model-callable tool in a DSH 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").

What are the most common mistakes when writing a DeepSeek Harness plugin?

**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").

How do you verify a DeepSeek Harness plugin is written correctly?

**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