DeepSeek Harness plugin debugging: --patch, dump-config

Plugin DevelopmentPublished 2026-09-12Author: DeepSeek Plugin Market
DSH pluginDeepSeek Harnesslocal debugging--patchtroubleshooting
Debug a DeepSeek Harness plugin locally: mount source with a --patch overlay, inspect config with --dump-config, read Fiber states, and fix stale changes.

Debugging a DeepSeek Harness plugin locally has two channels: while editing source, mount the plugin file by absolute path through a --patch overlay (restart to apply); to verify a distributable artifact, install it with dsh plugin --profile <name> add. The two verify different things, and mixing them makes "never mounted" look like "broken code." Every DSH plugin follows the same boundary between those channels.

Choosing between the two DSH plugin debug channels

There is one criterion: are you verifying loading or installation? (source)

ChannelCommandVerifiesApplies by
Overlaypnpm dsh web --patch ./scratch-plugin/cordis.ymlWhether the plugin loads, whether the code is rightSource edit + restart
Profile installdsh plugin --profile demo add ./hello-pluginWhether the packaged artifact installsRe-running add

Always start with the overlay when debugging source, because its feedback loop is the shortest: no complete package.json, no packaging, no pnpm install.

Mounting DSH plugin source with --patch

Two hard rules for overlays: the plugin path must be absolute, and a patch contributes configuration only. The official tutorial works in three steps (source):

  1. Get the absolute path: run pwd at the checkout root; what goes into the config must be the full absolute path.
  2. Write the overlay config: insert the plugin row in ./scratch-plugin/cordis.yml:
yaml
- insert:
    - id: hello
      name: '/absolute/path/to/deepseek-harness/scratch-plugin/src/my-plugin.ts'
  1. Boot with the overlay: pnpm dsh web --patch ./scratch-plugin/cordis.yml, which you should expect to load the plugin, with source edits applying on restart.

Why "it is in the patch but never loads" is usually not a syntax problem: a patch does not change the profile directory used to resolve module paths, so relative paths, symlinks, or a mistyped absolute path all fail silently.

Reading a DSH plugin's config tree with --dump-config

Debugging without looking at the config tree is coding with your eyes closed. dsh ships two switches that print the composed result without booting (source):

  1. Print the composed tree: dsh --profile demo --dump-config, which you should expect to include every patch layer applied.
  2. Print the default tree: dsh --profile demo --dump-default-config, useful as a baseline to compare against.

The composition order decides who wins:

  1. Each bundle's patch (in dsh.profile.bundles order)
  2. The profile's own cordis.patch.yml
  3. The home-level $DSH_HOME/cordis.patch.yml
  4. --patch overlays (highest precedence)

Later layers win, which is why an overlay can override profile config. With that order clear, "the config changed but nothing happened" becomes explainable.

Locating a DSH plugin load failure by Fiber state

A plugin's lifecycle is a state machine, and where it stops tells you the class of problem (source).

PENDING ──▶ LOADING ──▶ ACTIVE
                │
                └──▶ FAILED        ACTIVE ──▶ UNLOADING ──▶ DISPOSED
  • Never enters the state machine: the loader never resolved the module — go back to the path and the patch.
  • Stuck in LOADING or FAILED: a dependency is not ready (a service named in inject does not exist) or apply threw.
  • ACTIVE but wrong behavior: the plugin loaded fine; the problem is your registration logic or the event mode you picked.
  • Still running after unload: a manual resource was not handed to ctx.effect.

One disposer trap: disposers start in reverse order, but asynchronous disposers run concurrently and are not guaranteed to finish one by one; order-dependent cleanup must be merged into a single ctx.effect.

Five-step check when a DSH plugin change does nothing

Check from cheapest to most expensive; the first step usually hits.

  1. Is the patch passed? Is --patch in the start command, and is the file path correct?
  2. Did your line reach the tree? Run dsh --profile demo --dump-config and search for the plugin id.
  3. Is the path absolute? A relative path does not error; it silently does not load.
  4. Is the source actually rebuilt? Determine whether the entry artifact or the source is running, and whether the file you edited is what the entry imports.
  5. Is it overridden upstream? Check whether the same id is rewritten in the profile or home config (see the composition order above).

DSH plugin cleanup and unload verification

Verify cleanup while debugging, or the problem lands on users' machines instead. Two rules: things registered through ctx (event listeners, tools, timers) are cleaned up automatically on unload; manual resources (connections, file handles) only release if wrapped in ctx.effect() with a disposer (source).

How to verify: unload the plugin and watch whether logging stops and whether the service is still reachable. For service-level problems such as duplicate registration, see service already registered; if the plugin never activates at all, see plugin not activating.

A few easily forgotten commands and prerequisites to close with:

  • Running from source needs pnpm run build first (production runs require built artifacts), then pnpm dsh <args...> forwards every argument;
  • the launcher parses only its own flags, and the first token it does not recognize starts the app's arguments — the order cannot be reversed;
  • the web and headless profiles auto-initialize from templates on first use, while any other profile must be created through dsh plugin;
  • invalid commands, bad config, and boot failures all exit nonzero — a nonzero exit code is a real failure, not noise.

If the environment itself is not ready, see environment setup; after debugging passes, run the development spec checklist and continue to packaging into a bundle.

FAQ

Should a DeepSeek Harness plugin be debugged with --patch or a profile install?

**Debug a DeepSeek Harness plugin by purpose: use a --patch overlay to verify the plugin loads**, since a source edit plus restart is enough with no packaging; **use dsh plugin --profile <name> add <package> to verify the packaged artifact installs**. A patch contributes configuration but does not change the profile directory used to resolve module paths, so local plugin paths must be absolute (source: dsh CLI README).

Why do code changes in a DSH plugin fail to take effect?

Check three things first: **① the DSH plugin path is not absolute**; **② --patch was not passed at startup** (or the patch file path is wrong); **③ you edited source while a different build artifact is running**. Printing the composed tree with --dump-config confirms whether your line actually made it into the tree (source: dsh CLI README).

How do you tell where a DeepSeek Harness plugin failed to load?

**Read the DeepSeek Harness plugin's Fiber state to see where loading failed**: PENDING → LOADING → ACTIVE, and any failure in that path moves it to FAILED; unloading goes ACTIVE → UNLOADING → DISPOSED. **Stuck in LOADING or FAILED usually means a dependency (inject) is not ready or apply threw** — a module-resolution problem instead shows up as the plugin never entering the state machine (source: official "Plugin framework").

What is the plugin config layer order and why does it matter when debugging?

**A DSH plugin's config is composed in this order: each bundle's patch (in dsh.profile.bundles order) → the profile's own cordis.patch.yml → the home-level $DSH_HOME/cordis.patch.yml--patch overlays**. Later layers win, which is why an overlay can override profile config; only with this order clear can you explain a config change that seems ignored (source: dsh CLI README).

How do you confirm a DSH plugin cleans up its resources?

**To confirm a DSH plugin cleans up its resources, unload it once and watch two signals**: anything registered through ctx (event listeners, tools, timers) should be cleaned up automatically, while manual resources only release if they are wrapped in a ctx.effect disposer. Note that disposers start in reverse order but asynchronous ones run concurrently and are not guaranteed to finish one by one — merge order-dependent cleanup into a single ctx.effect (source: official "Plugin framework").

Related Terms

--patch overlay
A --patch overlay is a DSH plugin startup layer that puts an extra cordis.yml on top of the composed config tree to mount a local, unpublished plugin. It has the highest precedence, above profile and home config, but does not change the profile directory used to resolve module paths.https://github.com/deepseek-ai/deepseek-harness/blob/master/apps/cli/README.md
--dump-config
--dump-config is the DSH plugin debugging switch that prints the composed config tree (with all patch layers applied) without booting — the fastest way to confirm a plugin is mounted; --dump-default-config prints the default tree instead.https://github.com/deepseek-ai/deepseek-harness/blob/master/apps/cli/README.md
Fiber state
A Fiber is a DSH plugin's lifecycle state machine inside the framework: PENDING → LOADING → ACTIVE, with any failure becoming FAILED, and ACTIVE → UNLOADING → DISPOSED on unload. Start load debugging by seeing which state a plugin stopped in.https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/develop/framework/index.md
profile
A profile is the DSH plugin boot unit: an ordered stack of plugin bundle patches plus the user's own overrides. Its directory holds a package.json (with dsh.profile.bundles) and a cordis.patch.yml.https://github.com/deepseek-ai/deepseek-harness/blob/master/apps/cli/README.md

Sources