DeepSeek Harness plugin errors: loading, PENDING, execute

Plugin DevelopmentPublished 2026-09-12Author: DeepSeek Plugin Market
DSH pluginDeepSeek Harnessplugin errorsexecute contracttroubleshooting
DeepSeek Harness (DSH) plugin errors and fixes: why a plugin never loads, stays PENDING, fails in apply, or breaks the execute contract.

Most DeepSeek Harness (DSH) plugin errors during development share one trait: they do not throw — they fail silently. Mixing export forms means the plugin is never recognized, a missing inject leaves it in PENDING, returning prose from execute means Code Mode gets no fields, and one file read inside a card presenter crashes session replay. None of these are syntax errors, and your compiler will not warn you. This article organizes the frequent ones as symptom, cause, and fix. Every DSH plugin hits the same set of silent-failure traps.

DSH plugin error quick reference

Find the symptom you can see, then jump to its section. (Based on the official Your first plugin and Tool authoring reference pages.)

SymptomCauseFix
Plugin is not recognized at all, no log outputExport form does not match the written formNamed exports for function form, default export for object/class form, never both
Stuck in PENDINGMissing inject, or a hard dependency written as ctx.get()Declare required services in inject
State moves to FAILED with a stack traceapply throwsRemove blocking work and one-shot side effects
Tool produces output but callers get no fieldsexecute returned prose instead of a canonical valueReturn one value matching output.schema
Cards crash or drift during session replayPresenter is not pureMove I/O and clocks out of presentCall / presentResult
Installed plugin does nothing, absent from the config treeBundle omits cordis.patch.yml, or files excludes itAdd dsh.bundle.patch and list the patch in files
Host crashes right after installgit distribution ships no build outputPublish to npm, or have the author add prepare
Timers keep running after unloadManually created resources never handed to ctx.effectReturn a disposer from ctx.effect()

DSH plugin loading and state errors

A plugin with no logs, stuck in PENDING, or moved to FAILED is failing at load time — check the export form, the inject declaration, and what apply does as a side effect.

Error 1: the plugin never enters the state machine

Symptom: the config tree has an entry, but the plugin logs nothing and has no Fiber state.

Cause: the framework reads your module as exactly one form. Function form uses named exports; object and class forms use a default export (source):

ts
// Function form: named exports.
export const name = 'my-plugin'
export function apply(ctx: Context) { /* ... */ }

// Object form: default export.
export default { name: 'my-plugin', inject: ['tools'], apply(ctx) { /* ... */ } }

The most common mix-up is writing both export default and a named apply — the framework reads the default export and silently ignores the other apply, and vice versa.

Fix: keep exactly one form per module, with name, inject, and apply in the same export. Full form selection is covered in DSH plugin development spec.

Error 2: a DSH plugin stuck in PENDING

Symptom: the plugin sits in PENDING and none of the code inside apply ever runs.

Cause: PENDING means the plugin is declared but its required services are not ready. A plugin that declares inject waits for every required service before apply runs, so an injected name that does not exist means it waits forever (source).

The second, sneakier cause is writing a hard dependency as an optional lookup:

ts
// Wrong: a hard dependency as an optional lookup; the plugin reaches ACTIVE half-broken.
export function apply(ctx: Context) {
  const tools = ctx.get('tools')
  tools?.register(/* ... */)
}

// Right: declare it in inject, and the plugin simply does not load without it.
export const inject = ['tools']
export function apply(ctx: Context) {
  ctx.tools.register(/* ... */)
}

Fix: ask whether the plugin "cannot work without the service" or "does one extra thing when it is there" — the first goes into inject, only the second uses ctx.get(). For a plugin that installs but never activates, see plugin not activating.

Error 3: a DSH plugin's state moves to FAILED

Symptom: the Fiber state becomes FAILED and the startup log carries a stack trace.

Cause: a synchronous throw inside apply fails the load. Three patterns cause most of them:

  1. Blocking work inside apply: apply is the synchronous initialization phase; network requests and large file reads belong in a background job.
  2. Ordering assumptions: inject guarantees services are ready, not that your callbacks run in the order you imagine.
  3. One-shot global side effects: when a dependency disappears the plugin is automatically unloaded and reloaded when it returns, so apply runs again (source).

Fix: keep apply light and repeatable. If it still throws, confirm with local debugging and --dump-config that the code actually mounted is the code you edited.

DSH plugin contract and rendering errors

Tool and card mistakes never throw — they only make fields unavailable or replay crash. The contract lives in what execute returns; rendering lives in presenter purity.

Error 4: breaking the execute contract

Symptom: the tool shows output in the UI, but in Code Mode await tools.xxx() yields a string and no ids or fields.

Cause: execute must return exactly one canonical JSON value. The registry snapshots that value as lossless JSON, validates it, freezes it, and passes it to output.render(args, value) for model-facing content. Returning content blocks from the body, or making callers parse prose, breaks the contract (source).

ts
// Wrong: model-facing prose used as the return value.
async execute(args) {
  return [{ type: 'text', text: `wrote ${args.path}` }]
}

// Right: return a structured canonical value and leave prose to render.
output: {
  schema: { type: 'object', properties: { path: { type: 'string' }, bytes: { type: 'number' } } },
  render: (args, value) => [{ type: 'text', text: `wrote ${value.path} (${value.bytes} bytes)` }],
},
async execute(args) {
  const bytes = await write(args.path)
  return { path: args.path, bytes }
}

Four more hard rules live in the same section — check them all before publishing:

  • A throw means isError: throw for infrastructure failures; represent a non-ideal but successful domain outcome inside the canonical value (the same applies to a whitelisted card render intent).
  • Honor exec.signal: cancel in-flight work when it fires.
  • Do not mutate the definition after registration: registration borrows your readonly definition, so do not change its schema or replace callbacks; to hot-swap, dispose the owning effect and register the replacement.
  • Args are validated, readonly input: defineTool validates model-generated arguments against the schema first, so execute receives inferred types; only constraints the DSL cannot express (non-empty strings, positive numbers, cross-field rules) are yours to check by hand.

The full tool-side walkthrough is in how to write a tool plugin.

Error 5: impure DSH plugin card presenters

Symptom: live runs look fine, but replaying a session crashes the card or shows content that does not match what happened.

Cause: presentCall / presentResult run on live streaming and on session-log replay, so they must be pure functions of args plus the result: no I/O, no reading session state, no clock or random. Reading a file's previous content or the working directory inside presentCall crosses that line (source).

The sibling mistake: leaking UI-only formatting into the model result. Fenced console blocks, inline diffs, and relativized paths do not belong in the canonical value or the model-facing content merely to serve a UI. output.render owns model prose; presentationMeta plus the presenters own replayable card state.

Fix: when a card needs result-time facts (applied hunks, for instance), derive replayable JSON from the same canonical value with output.presentationMeta(args, value) and hand it to presentResult — never read the filesystem for a card. One built-in cushion: defineTool only soft-validates the display path, so malformed or older logged arguments make the presenter return undefined and fall back to a generic card instead of throwing, which keeps replay from crashing.

DSH plugin distribution and install errors

Exit code 0 on install does not mean the plugin works: the config layer may never reach the profile, and the build output may never be in the package.

Error 6: installed but never mounted

Symptom: installation succeeds with exit code 0, yet the config tree has no plugin entry.

Cause: a distributed plugin ships as a bundle: package.json points dsh.bundle.patch at cordis.patch.yml, and that config layer is what inserts the plugin entry into the tree. Ship only index.js, or leave cordis.patch.yml out of files, and installers never receive the config layer.

json
{
  "files": ["index.js", "cordis.patch.yml"],
  "dsh": { "bundle": { "patch": "./cordis.patch.yml" } }
}

Fix: verify files includes the patch before publishing, then confirm the entry landed with dsh --profile demo --dump-config. A related trap: patches replace whole lines rather than deep-merging, so write the entire line whenever you override a field. The packaging flow is in packaging a bundle.

Error 7: the host crashes right after installing a DSH plugin

Symptom: install from github:owner/repo finishes without errors, then the host exits with ERR_MODULE_NOT_FOUND on restart.

Cause: a git distribution pulls source and does not run a build. If the repository ignores lib/ while main points at lib/index.js, the installed package has source but no entry artifact. Nothing surfaces at install time — checking the install command's exit code is nowhere near enough.

Fix: in order of preference — (1) publish to npm, where the tarball carries build output and installers need no build permission; (2) have the author add a prepare script and let users opt in through allowBuilds in the profile; (3) distribute a pnpm pack tarball meanwhile. The full comparison is in publishing to npm.

DSH plugin resource cleanup errors

Unloading is not stopping: resources created outside ctx must be handed back to the framework by you.

Error 8: resources that outlive unload

Symptom: after the plugin unloads, its timer keeps logging or its connection stays open.

Cause: anything registered through ctx is tracked and revoked on unloadctx.on, ctx.tools.register, ctx.llm.registerAdapter, and ctx.effect are all covered, so you never hand-write removeListener or clearInterval (source). What leaks is resources created outside ctx: network connections, file handles, and your own setInterval timers.

Fix: wrap those in ctx.effect() and return a disposer.

ts
export function apply(ctx: Context) {
  ctx.effect(() => {
    const timer = setInterval(() => console.log('heartbeat'), 5000)
    return () => clearInterval(timer)   // Runs when the plugin unloads.
  })
}

One detail worth memorizing: disposers start in reverse registration order, but multiple async disposers run concurrently and are not guaranteed to finish one by one. Cleanup with ordering dependencies (close the stream, then the connection) must live inside a single ctx.effect(), because splitting it removes the guarantee.

Catching DSH plugin errors before publishing

Only error 3 hands you a stack trace; the other seven fail silently — so "it installed" is never an acceptance criterion. The minimum pre-release routine:

  1. Run dsh --profile demo --dump-config and confirm your entry is in the config tree (catches error 6).
  2. add the plugin into a clean profile, restart the host, confirm it does not crash (catches error 7).
  3. Unload it once and check that logs and connections actually stop (catches error 8).
  4. Call the tool from Code Mode once and confirm you get structured fields (catches error 4).

For the rule-by-rule checklist see DSH plugin development spec; for debugging channels and config layering see local debugging; for silent config fallbacks see how to define plugin config. To compare against real plugin layouts and bundle declarations, browse DSH Plugin Hub.

FAQ

My DSH plugin never enters the state machine at all. Is the code wrong?

**When a DSH plugin never enters the state machine, check the export form first**: **function form requires named exports (export const name plus export function apply); object and class forms require a default export (export default).** Mixing them means the framework reads only one, and the other declaration is silently ignored — the usual mistake is writing both export default and a named apply, so the module is never recognized as a plugin (source: the official "Your first plugin" page).

My DSH plugin stays in PENDING. What causes that?

**A DSH plugin staying in PENDING means it is declared but its required services are not ready yet.** **Either a required dependency is missing from inject, or a hard dependency was written as an optional ctx.get() lookup.** The second case is sneakier: the plugin reaches ACTIVE in a half-broken state, which looks like "installed but the feature does nothing" (source: the official "Plugins and lifecycle" page).

What happens when apply() throws in a DSH plugin?

**When a DSH plugin's apply() throws, the plugin moves to FAILED and the stack trace shows up in the startup log.** Look for three things: **blocking work inside apply, ordering assumptions between callbacks, and one-shot global side effects.** A plugin that declares inject is automatically unloaded when a service disappears and reloaded when it returns, so apply must be repeatable (source: the official "Plugins and lifecycle" page).

What is a legal return value for a tool's execute()?

**A DSH plugin tool's execute() must return exactly one canonical JSON value that matches output.schema.** Do not return content blocks from the body, and do not make callers parse prose for ids and fields — model-facing prose belongs to output.render. Throwing, or returning a value that fails the schema, is treated as isError. Honor exec.signal and cancel in-flight work when it fires (source: the official "Tool authoring reference").

Why does my DSH plugin card crash when a session is replayed?

**A DSH plugin card crashes on session replay because its presenters (presentCall / presentResult) are not pure functions.** They run on live streaming and on session-log replay, so they must be pure functions of args plus the result: no I/O, no reading session state, no clock or random. If you want the file's previous content or the working directory inside presentCall, that belongs to durable result metadata or the UI adapter — not the presenter (source: the official "Tool authoring reference").

Related Terms

canonical value
A canonical value is the single authoritative value a DSH plugin tool's execute returns, matching output.schema. The registry snapshots it as lossless JSON, validates it, freezes it, then hands it to output.render for model-facing content, so callers get structured fields instead of parsing prose.https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/cookbook/adding-a-tool.md
presentCall / presentResult
presentCall / presentResult are the two UI presentation projections a DSH plugin tool declares: presentCall builds the PENDING card, presentResult builds the completed card. Both run on live streaming and on session replay, must be pure, and return a card-tagged render intent.https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/cookbook/adding-a-tool.md
PENDING
PENDING is the first state in a DSH plugin's Fiber state machine, meaning the plugin is declared but its required services are not ready. Staying in PENDING usually means an injected service does not exist, or a hard dependency was written as an optional lookup.https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/develop/framework/index.md
bundle
The distribution form of a DSH plugin: package.json points dsh.bundle.patch at cordis.patch.yml, and that config layer inserts the plugin entry into the config tree. The patch file must be listed in files, or installers never receive the config layer.https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/develop/basic/publish.md

Sources