A profile is a named composition under $DSH_HOME/profiles/<name>. Patches apply to an empty entry list in a fixed order: each bundle the profile stacks, then the profile's cordis.patch.yml, then the home-level $DSH_HOME/cordis.patch.yml, then any --patch overlay. Later layers win, and dsh --profile <name> --dump-config prints the composed result.
Most of the confusion people hit with DeepSeek Harness in its first week is a layering problem: they edit a config file, nothing changes, and it is not obvious why. The answer is almost always that a later layer overrode it, or that they edited the wrong one of two identically-named files.
The composition model is small enough to hold in your head. It is worth ten minutes.
What a profile is
A profile is a named composition stored in the harness home directory, at
$DSH_HOME/profiles/<name>. It does three jobs:
- lists the bundles it stacks,
- holds any out-of-tree plugins it installs,
- keeps the user's own
cordis.patch.yml.
Two templates ship with the preview: web and headless. You boot one with --profile:
dsh --profile web
dsh --profile headless "summarise the failing tests"dsh web is shorthand for the first. The headless form takes a job string, runs one session, emits
the result and exits — that is the shape you want in CI.
What a bundle is
A bundle is a distribution format for Cordis config rows and the code they mount. The important property is in that definition: whatever a bundle inserts stays patchable by the layers above it. Bundles are not opaque — they are a starting set of rows you can then override.
The ones that ship:
| Bundle | Provides |
|---|---|
dsh-base | Model adapters, tools, persistence, sandbox and approval policy, settings, credentials, telemetry |
dsh-web-app | The browser UI layer |
dsh-headless | Single-run execution with no server |
dsh-base is the foundation every profile stacks. The web profile is essentially dsh-base plus
dsh-web-app; headless is dsh-base plus dsh-headless.
The patch order
This is the part to memorise. Patches are applied to an empty entry list, in this order:
- Each bundle, in the sequence the profile lists them
- The profile's
cordis.patch.yml—$DSH_HOME/profiles/<name>/cordis.patch.yml - The home-level
cordis.patch.yml—$DSH_HOME/cordis.patch.yml - Any
--patchoverlay passed at runtime
Later layers win. A patch targets a row by identifier and either replaces that row's whole config or inserts new rows.
Two practical consequences fall straight out of this:
Your home-level file beats your profile file. If you set something per-profile and it is being ignored, check whether you also set it at home level months ago and forgot. This is the single most common "my config does nothing" report.
--patch beats everything. That makes it the right tool for a one-off experiment — try a
different model or a stricter approval policy for one run without touching any file on disk:
dsh --profile web --patch ./experiments/strict-approval.ymlSeeing what actually booted
Never reason about the composed result from the source files. Print it:
dsh --profile web --dump-configThis shows every row that can be modified through patches, after all four layers have been applied. When behaviour and expectation disagree, this output is the arbiter.
The companion flag answers a different question:
dsh --profile web --dump-default-config--dump-default-config displays the composed tree without initialising the profile — what a
clean profile would look like, unpolluted by your local edits. Diffing the two outputs is the fastest
way to see exactly what you have changed.
Installing plugins into a profile
Out-of-tree plugins live in the profile's own node_modules. The launcher exposes a subcommand that
forwards its arguments straight to pnpm:
dsh plugin --profile web add some-cordis-pluginBecause it is pnpm underneath, the full pnpm vocabulary works — add, remove, update, version
specifiers. Bundles are resolved from the dsh installation first, then from the profile's own
node_modules, which is what lets a locally installed plugin shadow or extend a shipped one.
Argument order matters
The launcher parses its own flags first. The first token it does not recognise begins the application's argument list. So this works:
dsh --profile web --port 3100...because --port is passed through to the booted app, not consumed by the launcher.
Invalid commands, options belonging to a different mode, configuration errors and boot failures all
exit nonzero. If you are wrapping dsh in a script, check the exit code — a misplaced flag fails
loudly rather than silently doing the wrong thing.
A working mental model
Think of it as CSS cascade for an agent runtime. Bundles are the user-agent stylesheet: sensible
defaults, fully overridable. The profile patch file is your stylesheet. The home patch file is your
!important. And --patch is the inspector, where you try a change before committing it.
Frequently asked
What is the difference between a profile and a mode?
A profile is the on-disk composition that boots — web and headless ship as templates. A mode is the tool surface the resulting agent presents. Profiles are the mechanism; modes are one thing that mechanism produces.
Which cordis.patch.yml wins, profile-level or home-level?
Home-level. Patches apply in order — bundles, then the profile's patch file, then the home-level patch file, then --patch — and later layers override earlier ones.
How do I see the configuration that actually booted?
Run dsh --profile <name> --dump-config. It prints every row that patches can target, after composition. Use --dump-default-config to see the composed tree without initialising the profile.
Where do out-of-tree plugins get installed?
Into the profile's own node_modules. Use dsh plugin --profile <name> <pnpm args>, which forwards to pnpm.