dshkit

DeepSeek Harness profiles and patch layers explained

How a dsh profile composes bundles into a running agent: the four-step patch order, cordis.patch.yml at profile and home level, the --patch overlay, and using --dump-config to see what actually booted.

Updated 2026-08-134 min
Short answer

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:

  1. lists the bundles it stacks,
  2. holds any out-of-tree plugins it installs,
  3. 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:

BundleProvides
dsh-baseModel adapters, tools, persistence, sandbox and approval policy, settings, credentials, telemetry
dsh-web-appThe browser UI layer
dsh-headlessSingle-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:

  1. Each bundle, in the sequence the profile lists them
  2. The profile's cordis.patch.yml$DSH_HOME/profiles/<name>/cordis.patch.yml
  3. The home-level cordis.patch.yml$DSH_HOME/cordis.patch.yml
  4. Any --patch overlay 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.yml

Seeing what actually booted

Never reason about the composed result from the source files. Print it:

dsh --profile web --dump-config

This 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-plugin

Because 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.

Keep reading