dshseek

seek://guides/plugin-101

Plugin 101: Your First DeepSeek Harness Plugin

Aug 15, 2026Intermediate← All guides

TL;DR

A plugin is a TypeScript module exporting apply(ctx) — that's the whole contract. This guide builds one from scratch the official way: develop it live through a cordis.yml patch layer, then package it as an installable bundle with a dsh.bundle manifest, and understand why bundle and profile are two different nouns.

Key points

  • The entire plugin contract: `export const name` plus `export function apply(ctx)` — capabilities register through the Cordis context
  • Local development uses a patch overlay: a cordis.yml with absolute paths, loaded via `dsh web --patch`
  • Everything registered through ctx auto-cleans on unload; manual resources use `ctx.effect()`
  • Distribution is an npm package whose package.json carries `dsh.bundle` pointing at a patch file
  • Bundles contribute layers; profiles compose them — `dsh plugin add` installs a bundle into a profile

The fastest way to understand “everything is a plugin” is to write one. The contract is small enough to hold in your head — smaller than most HTTP frameworks’ middleware — and everything else is convention on top of it.

The whole contract

A plugin is a TypeScript module that exports a name and an apply function:

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

export const name = 'hello-plugin'

export function apply(ctx: Context) {
  // Required dependencies are ready before apply runs.
  console.log('[hello-plugin] plugin loaded!')
}

That’s it. The framework loads the module, calls apply with a context object, and you register capabilities through that ctx — tools, event listeners, timers, UI contributions. The context is the entire API surface; the architecture’s promise is that this surface has no second class.

One detail worth internalizing early: everything registered through ctx cleans itself up on unload. Event listeners, tools, timers — you never write removeListener or clearInterval for them. For resources that need manual teardown (a network connection, a child process), there’s ctx.effect():

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

This is why plugins compose: nothing leaks when they leave.

Develop it live: the patch layer

You don’t install your plugin during development — you patch it in. Working from a harness checkout (the tutorial’s assumption), create a scratch project and an overlay file:

mkdir -p scratch-plugin/src
# scratch-plugin/cordis.yml
- insert:
    - id: hello
      name: '/absolute/path/to/deepseek-harness/scratch-plugin/src/my-plugin.ts'

The path must be absolute — patch files contribute configuration only; they don’t change how the loader resolves modules. Then launch with the overlay:

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

Open http://127.0.0.1:3080 and watch the terminal: [hello-plugin] plugin loaded!. You are now developing against the live Web UI with your plugin mounted. This insert/patch mechanism is not a debugging hack — as the next section shows, it is the distribution format.

Package it: the bundle

To distribute, turn the plugin into a bundle — an npm package whose package.json declares a dsh.bundle manifest:

hello-plugin/
├── package.json       # declares dsh.bundle
├── cordis.patch.yml   # the layer applied when a profile lists this bundle
└── index.js           # plugin modules the patch rows reference
{
  "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" } }
}

Read the manifest as answering one question — what does this package contribute? — with the answer being: a patch file that inserts or overrides plugin rows.

Two nouns, two manifests

The system runs on a deliberate two-word vocabulary, and conflating them is the classic beginner mistake:

  • A bundle is what you write and publish: an npm package declaring dsh.bundle, contributing one config layer.
  • A profile is what a user launches: a directory under $DSH_HOME/profiles/<name> declaring dsh.profile, describing which bundles, in what order, compose the launched configuration.

Layer order decides the final merged configuration — which is why a profile is an ordered composition rather than a set. Users install your bundle into a profile with dsh plugin add and launch with dsh --profile <name>. Nothing is both a bundle and a profile.

If you’ve used the Web UI, you’ve been running a profile all along — installing plugins adds bundles to it.

Ship it

  1. Publish the npm package. The files array must include the patch file — forgetting it is the classic broken-install bug.
  2. Tag the repo with the dsh-plugin GitHub topic — it’s the ecosystem’s discovery signal.
  3. Submit a line to the community awesome list: one line under the right category, both README languages.

One warning to carry with you, verbatim in spirit from the community list: installing a plugin runs third-party code with your permissions. Tool approvals don’t sandbox plugin code. Review sources before installing; publish code you’d let a stranger review. The ecosystem’s speed is a direct consequence of this trust model — honor it.

Where to go deeper

The official tutorial continues into configuration and tool registration (docs/user/develop/basic/), and the framework docs cover events and lifecycle. For a working reference, the community plugin template is a cleaned-up starting point — clone it, rename, ship.

Your first plugin is now fifteen lines away from a thing people can install. The architecture did that on purpose.

Official references