Plugins provide every agent capability in dsh — models, tools, skills, sessions, sandboxes, storage, loops, scheduling and the UI. They are distributed as bundles, which are config rows plus the code they mount, and anything a bundle inserts stays patchable by the layers above it. Bundles resolve from the dsh installation first, then the profile's own node_modules.
"Everything is a plugin" is the kind of claim that is usually marketing. In DeepSeek Harness it is closer to a literal description of the boot process, and it is worth understanding what follows from it, because it explains both the harness's flexibility and its sharp edges.
The claim, precisely
Plugins provide every agent capability: models, tools, skills, sessions, sandboxes, storage, loops, scheduling, and the UI. Cordis services and events are how those plugins reach each other.
The consequence is that there is no privileged core to configure around. When the harness boots, it composes a list of configuration rows, and every row came from a plugin. Which is why this command is meaningful rather than a debug curiosity:
dsh --profile web --dump-configIt prints the whole running system, and every row in it is something a patch can target.
Bundles: the distribution unit
You do not usually install a single plugin. You stack a bundle — a distribution format for Cordis config rows and the code they mount.
The definition carries a guarantee worth stating on its own: whatever a bundle inserts stays patchable by the layers above it. A bundle is a starting position, never a sealed box.
Three ship with the preview:
| Bundle | Provides |
|---|---|
dsh-base | Model adapters, tools, persistence, sandbox and approval policy, settings, credentials, telemetry |
dsh-web-app | Browser capability — the web UI |
dsh-headless | Single-run execution, no server |
dsh-base is the foundation everything stacks on. The rest is composition: web is base plus the
web app, headless is base plus headless. That is the entire difference between the two shipped
profiles.
Why this makes the UI swappable
If the browser UI arrives as a bundle rather than as the program itself, replacing it is a
configuration change. The same reasoning applies to the sandbox policy, the storage layer and the
approval gate — each is a row inserted by dsh-base, and each can be replaced by a patch layer
above it.
This is the real payoff of the architecture, and it is why "harness" is a better word than "app". A harness is the thing you strap around a model; DeepSeek's bet is that you should be able to restrap every part of it without forking.
Installing out-of-tree plugins
Third-party plugins install into the profile's own node_modules:
dsh plugin --profile web add some-cordis-pluginThe subcommand forwards its arguments straight to pnpm, so the full pnpm vocabulary works —
add, remove, update, version specifiers, workspace protocols.
Resolution order matters when a name could refer to two things: bundles resolve from the dsh
installation first, then from the profile's own node_modules. Combined with patch layers, this
gives you two independent levers — you can shadow a package by name, or leave the package alone and
override the specific rows it inserted.
Because plugins are per-profile, two profiles can hold incompatible plugin sets without interfering. That is the sane way to run an experimental configuration next to the one you actually depend on: clone the profile, install the risky plugin into the clone, and boot whichever you want by name.
Patching a plugin's rows
A patch targets a row by identifier and either replaces its whole config or inserts new rows. The
layers apply in a fixed order — each bundle in the profile's sequence, then the profile's
cordis.patch.yml, then $DSH_HOME/cordis.patch.yml, then any --patch overlay.
Practically, the workflow is always the same three steps:
dsh --profile web --dump-configto find the row identifier you want to change- Write a patch targeting that identifier, in the profile's
cordis.patch.yml --dump-configagain to confirm the composed result is what you meant
Skipping step 3 is how people end up convinced the config system is broken when a higher layer is simply winning.
Where this bites
The flexibility is not free.
Every plugin is an API surface that can move. The harness is a v0.1 developer preview whose maintainers state core plugins and APIs will continue to evolve. A plugin you write against a specific row's shape is a thing you have agreed to maintain.
Debugging is composition-shaped. When behaviour surprises you, the question is rarely "what does
this code do" and usually "which layer set this". That is a different debugging instinct than most
tools require, and --dump-config versus --dump-default-config is the diff that answers it.
Nothing stops you composing something incoherent. A kernel that lets you replace the approval policy will let you replace it with one that approves everything. Read what you are patching before you patch a sandbox.
Frequently asked
Is the web UI really a plugin?
Yes. Browser capability comes from the dsh-web-app bundle layered on dsh-base. A headless profile simply stacks dsh-headless instead, giving single-run execution with no server.
How do I install a third-party plugin?
dsh plugin --profile <name> <pnpm args>, which forwards to pnpm and installs into that profile's own node_modules.
Can a local plugin override a shipped one?
Bundles resolve from the dsh installation first, then from the profile's node_modules, and patch layers above a bundle can replace any row it inserted. Overriding shipped behaviour is the designed path, not a hack.