Fix custom agent presets not loading in DeepSeek Harness
If you put a custom agent preset in your project directory — so it can ship with the repository and live under git — set agent-presets.roots in your config, and the Web UI session picker still never shows it, your YAML is not the problem. DeepSeek Harness's composeProfile() injects one extra overlay after composing every patch layer, replacing the whole roots on the agent-presets row with the shipped root only; it is passed to the loader as the last overlay, so roots declared at any layer is never scanned (#403).
Symptom: a DeepSeek Harness preset is in the project, but the picker does not show it
What makes this bug so costly is that it first makes you doubt your own config, while every self-check you run tells you the config is right. Concretely:
- The repro path is clean: place
agent.cordis.yml+preset.ymlunderD:\project\.dsh\agent-presets\my-preset\, then write a patch that injects roots:
# test.patch.yml
- id: agent-presets
config:
default: standard
includeUserRoot: true
roots:
- path: D:/project/.dsh/agent-presets
trust: user
Start with npx @deepseek-ai/dsh web --patch test.patch.yml, open the Web UI → new session → preset picker, and my-preset does not appear (#403).
2. A two-way comparison proves the preset files are fine: copy the same preset into <dshHome>/.agent-presets/my-preset/ (the includeUserRoot default behavior) and it is immediately visible. The file contents are identical; the only variable is the root's origin, so the fault is necessarily in the roots injection, not the preset (#403).
3. --dump-config gives you a "config is correct" illusion: --dump-config --patch test.patch.yml prints your roots plainly. This is not the dump being stale or cached — the array it prints was built before the override was pushed, so no CLI argument will show you what the loader actually receives (#403).
4. The failure looks intermittent rather than total: the overlay spreads your config and then replaces one key, so other fields on the same row (default, includeUserRoot) take effect normally while only roots is discarded. Anyone testing with "roots plus another field" sees the other field work and concludes the problem is flaky (#403).
5. Consistent across platforms and versions: the reporter located the behavior on Windows 11 / 0.1.0-rc.6; the community got a byte-for-byte identical source location on macOS / 0.1.1-rc.2; another confirmed it on 0.1.0-rc.7, and the profile-boot-DG5t9aNs.js filename hash being identical between rc.7 and rc.2 shows the module was not touched in between. So this is neither platform-specific nor a one-off in a single build (#403).
6. Blast radius: every deployment that wants presets inside the project — shipping with the repo, managed by git — is unusable; it directly contradicts the roots configuration table in the dsh-agent-presets README; and team-shared preset roots, plus multi-project multi-root setups, are simply not achievable (#403).
Mechanism: DeepSeek Harness composeProfile pushes a final overlay that replaces roots
The essence is not "the config was not read" but "the config was read, then rewritten before loading by a step you cannot see." Layer by layer:
- The overlay lives in the CLI: in
apps/cli/src/profile-boot.ts,composeProfile()(compiled outputlib/profile-boot-DG5t9aNs.jslines 179-187) contains:
if (rows.has("agent-presets")) composedOverlays.push({
id: "agent-presets",
config: {
...rows.get("agent-presets")?.config ?? {}, // keeps default/includeUserRoot
roots: [{ // ← roots replaced outright
path: SHIPPED_PRESET_ROOT,
trust: "system"
}]
}
});
Note the ordering: it spreads the user config first (so default and includeUserRoot survive), then hardcodes roots (so only roots is lost). That is exactly why the config looks half-respected (#403).
2. Why it necessarily wins: the overlay is pushed to the end of composedOverlays and handed to the loader as the last overlay. Later layers override earlier ones, so whether you set roots in the bundle, the profile, home, or --patch, the effective value is always "shipped root only" (#403).
3. Why --dump-config disagrees: the dump prints the patch layer composition, while the injection happens after the dump, before the loader mounts plugins. It is not stale data but a genuinely different value — which is why every documented self-check fails on this one field (#403).
4. Why the user root is the sole survivor: in dsh-agent-presets itself, includeUserRoot defaults to true and appends <dshHome>/.agent-presets as a user root after the roots computation this bug corrupts. It is therefore structurally immune, not lucky — an important distinction, because it means the workaround does not expire with a version change (#403).
5. A useful counter-proof: a community tool (dsh-blueprint) has always worked precisely because it writes only to the default user root and never touches roots. Another maintainer had documented a "copy into the CLI's shipped root" step for their own package and, after reading this thread, switched to the user root — copying into the CLI install tree must be redone on every upgrade, while the user root lives in user space and survives upgrades (#403).
Workaround and fix: DeepSeek Harness user root, overlay-check, and the append-style patch
What works today is the user root; a complete fix turns "replace" into "append"; and if you want a warning, you can add the check in your own tooling. Specifically:
- The most reliable workaround now — use the user root: the directory shape is below, and
agent.cordis.ymlis isomorphic to the shippedstandardpreset (apersonarow using@deepseek-ai/dsh-persona, then one- id: <row> / name: '<package>'per tool):
$DSH_HOME/.agent-presets/
my-preset/
preset.yml # name, description, order
agent.cordis.yml # persona row, then one flat row per tool
Discovery is unmemoized, so the preset is selectable the moment the files land, with no restart; and a directory whose composition is unparsable or is not a list of named rows is listed as broken with a reason rather than skipped silently — a genuinely nice touch when authoring by hand (#403). 2. The upstream fix direction — append instead of replace, letting user config take precedence:
if (rows.has("agent-presets")) composedOverlays.push({
id: "agent-presets",
config: {
...rows.get("agent-presets")?.config ?? {},
roots: [
...(rows.get("agent-presets")?.config?.roots ?? []), // user config first
{ path: SHIPPED_PRESET_ROOT, trust: "system" } // shipped as fallback
]
}
});
The key is demoting the shipped root to a fallback rather than the only entry, which fixes custom roots while keeping built-in presets visible (#403).
3. An interim step when the project root must ship with the repository: until the fix lands, copy or sync the in-project directory into the user root (a startup-script sync works too). That preserves git management while making the preset discoverable, and you can switch back to the project root once upstream appends instead of replaces (#403).
4. Turn the silent overwrite into a warning: the community has already packaged this check as a tool rule — dsh-overlay-check 0.4.0 (MIT, no dependencies) refuses to let an overlay claim this silently:
warning config-silently-overwritten "agent-presets.roots" is replaced at boot
with the shipped root, so setting it here does nothing — and --dump-config will
still show your value, because the override is applied after the composition it
prints. Put presets in $DSH_HOME/.agent-presets/<id>/ instead.
It is a plain function over patch rows, so anything that writes config can reuse it, not only the author's own tooling; the rule is deliberately scoped to this one row and one key so it can be deleted cleanly once upstream is fixed (#403).
5. A debugging habit worth adopting: when you hit the combination "the config dump is correct but runtime does nothing", suspect that the config is being rewritten before loading, rather than re-checking YAML syntax. A runtime probe (reading agentPresets.config) that shows the effective value beats any static check. The same applies when you distribute a preset-bearing plugin through DSH Plugin Hub: if your usage instructions tell users to configure roots, you are today pointing them down a road that does not arrive (#403).
DSH plugin troubleshooting notes
Remember first that nothing here is a YAML mistake — the config really is read; it is merely rewritten before loading by a step you cannot see, so every static self-check will tell you the config is right. Eight points to keep in mind when a DeepSeek Harness plugin ships presets:
- Do not trust
--dump-configalone: it excludes the composeProfile runtime injection, so it shows intent, not the effective value. - The user root is structurally immune:
includeUserRootappends after the corrupted computation, so the override cannot reach it. - The failure looks intermittent: other fields on the same row still apply; only
rootsis discarded. - Consistent across platforms and versions: Windows/Linux/macOS and rc.6/rc.7/rc.2 behave the same, so it is not a flake.
- The user root's advantage: it lives in user space, so a
dshupgrade needs no recopy — better than copying into the CLI install tree. - Discovery is unmemoized: files landing on disk are enough to be selectable, usually with no restart.
- The fix direction is append, not replace: the shipped root should become a fallback.
- A note for plugin authors: do not recommend configuring
rootsin your usage docs unless upstream is confirmed fixed.

Sources: Discussion #403, dsh-overlay-check.
FAQ
DeepSeek Harness replaces your configured roots wholesale at boot, so a preset inside the project is never scanned. After composing every patch layer, composeProfile() pushes one extra overlay that swaps the agent-presets row's roots for the shipped root only, [{ path: SHIPPED_PRESET_ROOT, trust: "system" }]. It is handed to the loader as the **last overlay**, so no matter which layer you set roots in, it loses.
In DeepSeek Harness --dump-config prints the **patch layer composition** (bundle + profile + home + --patch), while the override is pushed onto that array **after** it has been built. So the dump is not stale or cached — it is genuinely showing a **different value from the one the loader receives**. Nothing you can pass to the CLI will reveal the effective value, which is the part that costs people an afternoon.
Until the DeepSeek Harness fix lands, put the preset in the user root <dshHome>/.agent-presets/<preset-name>/ (on Windows the default is %USERPROFILE%\.dsh\.agent-presets\, and $DSH_HOME moves it). includeUserRoot defaults to true and appends **after** the roots computation this bug corrupts, so it is structurally immune; being in user space, it also survives a dsh upgrade without needing to be copied again.
The quickest confirmation is one controlled comparison: DeepSeek Harness --dump-config --patch test.patch.yml shows your roots, but the Web UI session picker simply does not list the preset — that is this same issue. Stronger evidence is a runtime probe (a dynamic plugin reading agentPresets.config) showing the runtime roots contains only the shipped one, or copying the same preset into the user root and seeing it appear immediately, which proves the preset files are fine and the roots injection is the problem.
Related Terms
- composeProfile
- The CLI function that composes bundle, profile, home, and --patch layers into the final config at startup. Besides composing, it injects one extra overlay dedicated to the agent-presets roots, and that injection step does not appear in --dump-config output.— https://github.com/deepseek-ai/deepseek-harness/discussions/403
- shipped root
- The built-in preset root distributed with the CLI (SHIPPED_PRESET_ROOT), with trust marked as system. It is the only roots entry the override keeps, which is why built-in presets are always visible while custom roots never are.— https://github.com/deepseek-ai/deepseek-harness/discussions/403
- includeUserRoot
- An agent-presets option, defaulting to true, that appends <dshHome>/.agent-presets as a user root. The key point is that it appends after the roots computation the override corrupts, making it the one root immune to the composeProfile runtime override.— https://github.com/deepseek-ai/deepseek-harness/discussions/403
Sources
- deepseek-harness Discussion #403: agent-presets.roots user config is overwritten by composeProfile, custom preset roots never take effect· deepseek-ai (GitHub Discussions)
- dsh-overlay-check 0.4.0 (MIT, config-silently-overwritten rule)· GitHub (taltara)