A starter template for dsh plugins covering config, tools, events, services, hooks, browser UI slots, and slash commands.
Install
# from GitHub (first run asks for allowBuilds approval — follow the hint, retry)
dsh plugin --profile web add github:kun2-5code/dsh-plugin-template
Any plugin you install runs third-party code with your own permissions — it can read your files, use your credentials, and reach the network, and tool approvals don’t sandbox it. GitHub-sourced plugins also run build scripts at install time. Only install sources you trust, and pin a commit (github:owner/repo#sha).
README
English | 简体中文
A ready-to-run, ready-to-install starter template for DeepSeek Harness (dsh) plugins. It demonstrates the six most common plugin shapes in one minimal installable bundle:
- Config —
Configinterface + Schemastery schema; validation and defaults apply at load time (docs) - Tool —
ctx.tools.register(defineTool(...))registers a model-callable tool (docs) - Events —
ctx.on/ctx.emitwith declaration merging for typed events (docs) - Service — a class-form plugin that provides a service to other plugins (docs)
- Hook — a
tools/pre-executepermission gate that denies tool calls by config (docs) - Browser half (client) —
src/client/registers browser UI on fourteen surfaces (index: docs/ui-surfaces.md): a clickable config card under Settings → Plugins → Configurable (writesgreeting/maxRetries/verboseinto the settings document, taking effect live; on a stock harness the card renders a read-only "not exposed" explainer instead of vanishing), a sidebar footer action button, an input dock strip above the composer, a shell overlay pill, a header utility badge, input tool-row buttons (left/right), a custom command row for/dsh-demo, a General settings row, a Plugins tab, a settings header action, a session header action, a composer dock strip, and per-message actions on AI replies — plus apresentResultrender intent on thegreettool. Only the config card's data path is gated by the harness allowlist; the other thirteen are pure slot registrations that work on any harness.
The template follows the official bundle distribution model: the package declares dsh.bundle plus cordis.patch.yml, and dsh plugin add activates it as a config layer.
Directory structure
dsh-plugin-template/
├── package.json # npm manifest + dsh.bundle / dsh.client declarations + prepare build script
├── tsconfig.json # strict type-check configuration (tsc --noEmit)
├── tsdown.config.ts # build config: Node library (lib/) + client bundle (lib/client.js), self-contained for git-install prepare
├── cordis.patch.yml # bundle config layer: inserts the plugin rows
├── dev/cordis.yml # local dev overlay (points at source; use with dsh web --patch; host half only)
├── docs/
│ └── ui-surfaces.md # where the plugin registers UI + index of every slot (bilingual: ui-surfaces.zh.md)
├── src/
│ ├── index.ts # main plugin: Config + tool + events + effect, config wired through the settings namespace
│ ├── commands.ts # host half: demo slash commands /hello (replies world) and /dsh-demo (custom row)
│ ├── service.ts # optional example: Service provider (disabled by default)
│ ├── hook.ts # optional example: hook permission gate (disabled by default)
│ └── client/ # browser half: one module per UI surface (see docs/ui-surfaces.md)
│ ├── index.ts # client entry: inject + apply, assembles the registrations
│ ├── constants.ts # shared NAMESPACE + DEMO_COMMAND_NAME (keep in sync with package.json name / cordis.patch.yml)
│ ├── types.ts # minimal structural types for ctx services (no @deepseek-ai client imports)
│ ├── styles.ts # one injected <style> with all dtpl-* classes (theme tokens only)
│ ├── config-card.ts # settings.plugin.item: the clickable config card (staged form + status states)
│ ├── sidebar-action.ts # sidebar.footer.action: sidebar-footer button
│ ├── input-dock.ts # conversation.input.dock: strip above the composer (session-scoped)
│ ├── shell-overlay.ts # shell.overlay: frame-wide floating pill
│ ├── header-utilities.ts # conversation.session.header.utilities: right-aligned header badge
│ ├── input-left.ts # conversation.input.left: tool-row control at the left end
│ ├── input-right.ts # conversation.input.right: tool-row control next to send
│ ├── commandview.ts # conversation.chat.commandview: custom row for /dsh-demo
│ ├── general-item.ts # settings.general.item: one preference row in Settings → General
│ ├── plugins-tab.ts # settings.plugins.tab: a new tab in the Plugins page
│ ├── settings-action.ts # settings.action: button in the settings header
│ ├── header-actions.ts # conversation.session.header.actions: session-title action button
│ ├── composer-dock.ts # conversation.composer.dock: strip under the composer card
│ └── assistant-actions.ts # conversation.chat.assistant-actions: per-message button
└── test/smoke.mjs # smoke test on the build output (incl. settings wiring unit test)
Quick start
Install as a bundle (for users)
From any directory, install this package (or your fork) into a dsh profile:
# local directory
dsh plugin --profile demo add /path/to/dsh-plugin-template
# or directly from GitHub (replace with your own repo after forking)
dsh plugin --profile demo add github:you/dsh-plugin-template
A GitHub install pulls source; pnpm runs prepare (i.e. tsdown) to build lib/. On pnpm ≥10 the first git-dependency prepare is refused; add the package name pnpm prints to the profile's pnpm-workspace.yaml and retry:
allowBuilds:
dsh-plugin-template: true
This allowlist authorizes executing that package's code at install time — only allow source you trust, and prefer pinning a commit:
github:you/dsh-plugin-template#<sha>.
Verify the config layer and boot:
dsh --profile demo --dump-config # should show a "# == dsh-plugin-template" layer
dsh --profile demo
Note: a custom-named profile (e.g.
demo) contains onlydsh-baseand is headless (no GUI). For the Web GUI and the config card below, use thewebprofile (= dsh-base+dsh-web-app) — see testing the config card.
Local development (modifying the plugin)
From the root of a deepseek-harness source checkout, load this repo's source directly via an overlay (no install, no build):
pnpm dsh web --patch /absolute/path/to/dsh-plugin-template/dev/cordis.yml
Set name in dev/cordis.yml to this repo's absolute path on your machine, open http://127.0.0.1:3080, and ask the model to call the greet tool.
⚠️ A
--patchoverlay only loads the plugin's host half (module resolution cannot reach package-level declarations). To test the browser-half config card you must install into a profile (resolved byname: dsh-plugin-template) — see the next section.
Run the checks yourself during development:
pnpm install
pnpm typecheck
pnpm build
node test/smoke.mjs
If this repo sits INSIDE a
deepseek-harnesscheckout (nested, as in the harness repo root),pnpm installis captured by the parent workspace and installs nothing here — the template is not a workspace member. Usepnpm install --ignore-workspace(pnpm ≥9.5) so the template installs its ownnode_modulesfrom its own lockfile; or clone the template standalone.
Testing the config card (in the GUI)
The config card renders in the browser and depends on dsh's client-modules discovering the dsh.client declaration by package name, so the package must be installed into a profile (a --patch source path won't do):
# 1. Build (produces lib/index.js + lib/client.js)
cd /path/to/dsh-plugin-template && pnpm build
# 2. Install into the web profile (= dsh-base + dsh-web-app, full GUI)
dsh plugin --profile web add /path/to/dsh-plugin-template
# 3. Boot the web GUI (`dsh web` is equivalent to `dsh --profile web`)
dsh web
Open http://127.0.0.1:3080:
- Bottom-left Settings → Plugins → Configurable tab: you should see a
dsh-plugin-templatecard. On a stock harness it renders a read-only "not exposed" status card (see below); after the one-line harness change it renders the editablegreeting/maxRetries/verbosefields; - Change
greeting, click Save — the status line should confirm it takes effect immediately; - Back in a session, ask the model to call the
greettool — you should see the new greeting (the host half reads the resolved namespace value live, no restart); - The change lands in the settings document (
settings.yamlunder$DSH_HOME) and survives restarts; to restore a default, edit the field back or clear it in the card.
After editing the client half (src/client/), rerun pnpm build and refresh the page (the client bundle's rev query cache-busts).
The config card on a stock harness (no source edits)
The card is a browser plugin (src/client/config-card.ts) that binds the settings namespace dsh-plugin-template through the settingsScope service. It always renders — but on a stock harness it shows a read-only "not exposed" status card instead of editable fields. Why: dsh's web gateway serves settings namespaces only from an explicit allowlist (WEB_SETTINGS_NAMESPACES in packages/host/apiproxy/src/api-proxy.ts), and a namespace absent from it answers settings-not-exposed even when its owner plugin registered it. This is a harness-side registration decision (the same source comment calls moving the declaration into settings.register() "deferred work"), not a template defect: the built-in cards render because their namespaces (shell, agent-loop, …) are allowlisted, and there is currently no plugin-side channel to add one — the gateway's RPC map is compile-time fixed and no registration flag exists yet.
What works on a stock harness with zero edits:
- the entire host half — the
greettool, events, the service, the hook gate — including live config reads: writes are only gated at the web RPC, the plugin itself reads the resolved namespace value on every execution; - the card slot itself: the card appears under Settings → Plugins → Configurable and explains the exposure state instead of vanishing silently.
To make the card editable, pick one:
- add
'dsh-plugin-template'toWEB_SETTINGS_NAMESPACESinpackages/host/apiproxy/src/api-proxy.ts(one line; rebuild/restart the harness; lost when you update the checkout):
const WEB_SETTINGS_NAMESPACES = [
'agent-loop', 'shell', 'locale', 'permission', 'ui-conversation', 'ui-theme', 'web-search-deepseek',
'dsh-plugin-template', // ← add this line
] as const
- wait for the harness's deferred work — moving the exposure declaration into
settings.register()— which this template already targets by registering the namespace the canonical way (installSettingsSection).
Making it your own plugin
- Rename the package: keep
package.jsonname(npm name, e.g.dsh-my-plugin),src/index.tsname, andcordis.patch.ymlid/nameconsistent; when renaming the./servicesubpath, updateexports/filestoo. Renaming also touches browser-half spots: the client bundleidintsdown.config.ts(__ModuleLoader__.load({ id })),NAMESPACEinsrc/client/constants.ts, anddsh.clientinpackage.json(if you needinject). - Change the
Configinterface andConfigschema: anything two deployments should be able to set differently must be a config field (design principles). The config is wired to the settings namespace — does the GUI card auto-render a form from your schema? No: the card insrc/client/config-card.tsis hand-written; add a field row there for each new config field. - Register your tool in
apply:ctx.tools.register(defineTool({...}));executereturns the canonical value declared byoutput.schema, andoutput.renderis the pure function for model-visible rendering (tool reference). - To provide capabilities to other plugins, enable
src/service.tsand uncomment its row incordis.patch.yml. - Remember to
declare module '@deepseek-ai/cordis'to mergeContext/Eventstypes — that is what keeps cross-package boundaries type-safe. - To intercept tool calls, act as a permission gate, or respond to system hooks, enable
src/hook.ts(uncomment itscordis.patch.ymlrow):ctx.on('tools/pre-execute', ...)returns{ kind: 'deny', reason }or callsnext()to allow (extension cookbook). - Config reads: every read in
src/index.tsgoes throughconfigSource()(the resolved settings-namespace value, falling back to the composition entry). If you derive registration-level facts from config inapply(e.g. register different tools by config), rebuild them ininstallSettingsSection'sonChangerather than reading only at execution points (see bash-local).
How the browser half works
package.jsondeclaresdsh.client: { platform: "web" }+exports["./client"]→ dsh's client-modules discovers it and loadslib/client.jsas a browser plugin;- the client entry (
src/client/index.ts) assembles one registration per UI surface — the config card (settings.plugin.item), the sidebar footer action (sidebar.footer.action), and the input dock (conversation.input.dock) — see the UI surfaces index; - the config card binds the
dsh-plugin-templatenamespace via thesettingsScopeservice: reads snapshots, stages drafts, and writes field-by-field on save (revision-fenced); - the host half (
src/index.ts) registers the same namespace withinstallSettingsSection(the cordis.yml config is thebaselayer) and reads the resolved value lazily in the tool → saving takes effect immediately; - at runtime the client half depends only on
react(provided by the browser platform module table); everything else goes throughctxservices and no@deepseek-aiclient package is imported — keep that discipline when editing the template.
Publishing
- npm:
pnpm publish(filesalready includes the build output and the patch; no extra steps) - tarball:
pnpm pack, thendsh plugin --profile demo add ./dsh-plugin-template-0.1.0.tgz - git:
dsh plugin add github:you/dsh-plugin-template(combined with theallowBuildsstep above)
Related docs
- Plugin development intro: basic/index.md
- Plugin config: basic/config.md
- Tool development: basic/tool.md
- Packaging & installation: basic/publish.md
- Plugins & lifecycle: framework/index.md
- Services & dependencies: framework/service.md
- Event system: framework/events.md
- Cordis tutorial: cordis-tutorial
Links
More in this category
strukto-ai/mirage#dsh★ 3467
Swaps the filesystem and bash providers for a mirage virtual workspace: file tools and shell commands run over mounted resources (RAM, S3, Redis, Slack, Gmail, Notion, Postgres) instead of the host disk, with per-mount read/write/exec modes, per-command sandbox routing (monty, pyodide, quickjs in process; docker, e2b, daytona remote), and installed CLIs (git, gh, slack, linear, ntn, gws, or one you register) as head words in the virtual terminal.
hust-open-atom-club/oh-dsh★ 215
Community distribution: TUI, desktop, and Web UI as one bundle with layered installation.
Jayden-X-L/forkprobe★ 66
Compare multiple skills on the same task and pick the winner.
vlln/plugin-registry★ 50
Ecosystem infrastructure: a thin browser console for managing official repository plugins (zero patches) plus a make-dsh-plugin skill for guided plugin development.
forrestchang/dsh-multica-runtime★ 39
Run the dsh runtime on Multica.
omdsh-dev/dsh-plugin-check★ 23
Plugin health checks: manifest protocol / patch format / build traps, zero-dependency and read-only.