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.
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>/: apackage.jsonwith the profile manifest (dsh.profile.bundles, an ordered list) and the user's owncordis.patch.yml. - A bundle is a distribution format for Cordis config rows plus the code they mount; it declares itself with
dsh.bundle.patchin its package.json. @deepseek-ai/dsh-baseis 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:
executereturns one canonical JSON value declared byoutput.schema;output.renderturns it into model-facing content. Throwing (or returning an invalid value) marks the call as an error.- Arguments are validated for you before
execute; honorexec.signalfor 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
| Goal | Mechanism |
|---|---|
| Add a model provider | register its adapter on ctx.llm |
| Add a model-facing capability | register on ctx.tools |
| Add shell execution | register a ctx.shell backend |
| Add filesystem access/policy | register a ctx.fs provider or listen to fs/* |
| Add a human command | register on ctx.commands |
| Add background work | register on ctx.jobs |
| Confine spawned processes | use a ctx.sandbox backend |
| Intercept a request/tool/turn | listen on agent/* or tools/* events |
| Add model-facing context | agent.inject() |
Next steps
- Your first plugin — the official tutorial
- Tool authoring reference — nested schemas, background work, UI cards
- Adding an LLM adapter
- Cordis tutorial
Related Articles & Guides
DeepSeek Harness — Agent Runtime
Complete guide to DeepSeek Harness (dsh), the MIT-licensed agent runtime where everything is a plugin. Covers installation, presets, model setup, plugins, and comparison to Claude Code and Codex.
Running the DeepSeek Harness TUI
Install and run the DeepSeek Harness terminal UI. Add the @tomowang/dsh-tui profile plugin, launch with dsh --profile tui, and pick providers with /model.
DeepSeek Harness Providers & Keys
Connect model providers to DeepSeek Harness. Add catalog or custom providers, configure settings.yaml, and wire in OpenCode Zen/Go API keys via the opencode-go route.