DeepSeek Harness Plugins & Extending

Understand the DeepSeek Harness plugin system: Cordis, profiles, bundles, configuration, and how to build a custom tool. Everything is a plugin.

August 21, 2026
deepseek-harnessdshpluginscordisdeveloptoolextend

Plugins & Extending

Everything in DeepSeek Harness is a plugin — models, tools, skills, sessions, sandboxes, loops, and the UI. The framework is Cordis, where plugins contribute services, typed events, and reversible effects to a shared context. There is no privileged core to patch: you extend dsh by mounting a plugin beside the others, and registrations are effects that unwind when the plugin unloads.

What a plugin is

A plugin is a TypeScript module that exports an apply function. The framework calls it on load with a ctx context object:

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

export const name = 'my-plugin'

export function apply(ctx: Context) {
  // Register capabilities here.
}

Anything registered through ctx is cleaned up automatically when the plugin unloads. For explicit resources (a network connection, a timer), provide a disposer:

export function apply(ctx: Context) {
  ctx.effect(() => {
    const timer = setInterval(() => console.log('heartbeat'), 5000)
    return () => clearInterval(timer)
  })
}

If the plugin consumes a service, declare it in inject; the framework waits for it before calling apply:

export const inject = ['tools']

There are three forms — function (most cases), object, and class (use the class form to provide a service to other plugins).

Profiles and bundles

A running dsh is a plugin tree composed at boot from ordered layers.

  • A profile is a named composition in $DSH_HOME/profiles/<name>/: a package.json with the profile manifest (dsh.profile.bundles, an ordered list) and the user's own cordis.patch.yml.
  • A bundle is a distribution format for Cordis config rows plus the code they mount; it declares itself with dsh.bundle.patch in its package.json.
  • @deepseek-ai/dsh-base is the first layer of every profile: model adapters, tools, persistence, sandbox/approval, settings, credentials, telemetry.

Layers apply over an empty root in this order: each bundle in the profile's listed order, then the profile's cordis.patch.yml, then the home-level $DSH_HOME/cordis.patch.yml, then any --patch overlays. A patch targets a row by id and replaces its whole config, or inserts new rows.

Inspect the tree your machine actually boots:

dsh --profile web --dump-config
dsh --profile web --dump-default-config

Configuration

Plugins accept configuration through cordis.yml. Export a Config type and a Schemastery schema with defaults on the fields:

import type { Context } from '@deepseek-ai/cordis'
import Schema from '@deepseek-ai/schemastery'

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

Declare the row in cordis.yml:

- insert:
    - id: hello
      name: './src/my-plugin.ts'
      config:
        greeting: 'Hi there'
        maxRetries: 5

Rules: anything two deployments may set differently must be a config field, invalid config fails the load with an actionable error, and a config edit hot-replaces the plugin (registrations unwind as effects).

Build a tool

A tool is a plugin registered on the tool registry. defineTool infers and validates arguments from a schema:

import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'

export const name = 'greet-tool'
export const inject = ['tools']

export function apply(ctx: Context) {
  ctx.tools.register(defineTool({
    name: 'greet',
    description: 'Greet someone by name.',
    parameters: {
      name: { type: 'string', required: true, description: 'The name to greet' },
    },
    output: {
      schema: { type: 'string' },
      render: (_args, value) => [{ type: 'text', text: value }],
    },
    async execute(args) {
      return `Hello, ${args.name}!`
    },
  }))
}

The contract:

  • execute returns one canonical JSON value declared by output.schema; output.render turns it into model-facing content. Throwing (or returning an invalid value) marks the call as an error.
  • Arguments are validated for you before execute; honor exec.signal for cancellation.
  • Registration is effect-based: disposing the plugin unregisters the tool, and the schema joins the system-prompt assembly automatically.

Load a local plugin over the running app:

dsh web --patch ./scratch-plugin/cordis.yml

Then ask the agent to use it, e.g. "Use the greet tool to greet Ada."

Where new behavior goes

GoalMechanism
Add a model providerregister its adapter on ctx.llm
Add a model-facing capabilityregister on ctx.tools
Add shell executionregister a ctx.shell backend
Add filesystem access/policyregister a ctx.fs provider or listen to fs/*
Add a human commandregister on ctx.commands
Add background workregister on ctx.jobs
Confine spawned processesuse a ctx.sandbox backend
Intercept a request/tool/turnlisten on agent/* or tools/* events
Add model-facing contextagent.inject()

Next steps