DeepSeek Harness · System Map

Profile / Bundle / Patch: How Far Users Can Reshape the Product

Swap deep capabilities without touching source: config is four layers of patches stacked onto an empty array — later wins, same id means whole-entry replace.

Course goalAfter this lesson you can explain three things: why DSH splits config into Bundle (release defaults), user patches (Profile layer + Home layer), and --patch (one-shot experiments), and how those layers prioritize; why a patch that hits the same entry does whole-entry replace, and which counterintuitive result that choice causes; and how dsh --dump-config lays out the final effective config for you.
Interactive demo · Patch layer sandbox

The sandbox below turns the four config layers into toggleable cards — bottom to top is application order. Hit Play and watch each layer brush its changes onto the final entry on the right. Watch layer 3 closely: it writes only one field, and the fields the two layers below carefully set get wiped. When you’re done, switch to deep-merge contrast mode and see another ending for the same change under merge semantics.

Semantics
Composed final entry · id: conversation-model
Empty entry list. DSH’s root config is just an empty array — the plugin tree is brushed on from scratch, layer by layer of patches.
Hit Play to start stacking, or flip each layer’s toggle first to change who participates. Scrolling here auto-plays once.
Teaching simulation: entry ids, fields, and filenames are simplified; stack order and replace semantics match apps/cli/src/profile-boot.ts lines 121–129 and vendor/include/src/index.ts lines 58–128. deep-merge mode is a teaching hypothesis — DSH does not implement that semantics.
First, nail three terms

Up front: DSH has no one big config file. The product shape is stacks of patch lists, and each stack has a clear owner.

Bundle is the release default. It’s an npm package; the substance is the patch list shipped inside. The three built-ins are base (shared core), web-app (browser surface), and headless (one-shot task mode).

Profile is a user’s assembly. It’s a directory under $DSH_HOME/profiles/<name>: the manifest lists which bundles to use and in what order, with the user’s own patch file beside it. First use of the names web or headless auto-initializes a template.

Patch is the smallest unit of change. One patch finds a target entry by id, then changes config, disables it, or inserts a new entry. Out-of-tree plugins enter here too: dsh plugin add installs into a profile, and afterward it’s just another patch layer.

Source: the three built-in bundles and how package.json declares bundles — see packages/bundle/README.zh.md; profile template auto-init at apps/cli/src/profile.ts lines 114–117.

Design idea 1 · Every config layer has an owner

What problem it solves

Imagine the opposite: one big config file where release defaults, this assembly’s customizations, personal prefs, and one-off experiments all live together. Three months later you upgrade the release — new defaults and your old edits tangle in the same file. You can’t tell which line who wrote, which line is safe to touch; upgrades become a manual diff.

There’s a more everyday failure: you temporarily try an experimental model, tweak config and forget to revert — next day the whole environment runs on experiment settings. The root cause is no ownership of change: who wrote it, what it rides with, when it should disappear — one file can’t answer those three.

What the idea is

DSH splits config into four layers, each with an owner: Bundle rides with the release, Profile with this assembly, Home with this machine, --patch with this one command. At boot it starts from an empty array and brushes the four patch layers on in fixed order — later wins.

The starting point really is an empty array: the root config file is just [], and the template comment says don’t edit this file — edit patch files. All real content arrives via patch insert, so every line in the final plugin tree can answer which layer it came from.

Four patch layers apply 1 to 4 in order — later wins 1 · Bundle layer Release defaults; rides with the dsh install 2 · Profile user layer Rides with this assembly 3 · Home user layer Machine-wide prefs; applies to every profile 4 · --patch overlay One-shot experiment; rides with the command Apply in order onto an empty array The only patch algorithm in the whole repo Final plugin tree boot() mounts and runs dsh --dump-config Same algorithm, offline compose
Same patch stack, same algorithm — mount and dump cannot drift apart.

The two user-patch layers also divide carefully: Profile rides with this assembly; Home is machine-local preference, applies to every profile, so it sits after Profile and overrides it. When both change the same id, Home wins. --patch comes last for one-shot experiments you don’t want in files — you can pass multiple, applied in CLI order. In long sessions both user-layer files are watched; save a change and the running tree recomposes in the same layer order.

Layer order in source is literally an array literal: the launcher flattens the composed profile into a patch array, and array order is application order. The function is short enough to quote as a slogan — it proves the four-layer sequence is hard-coded in one array, with no conditionals:

apps/cli/src/profile-boot.tslines 121–129
/** The full patch stack of one composed profile, in application order. */
function allPatches(composed: ComposedProfile): PatchOptions[] {
  return [
    ...composed.bundlePatches,
    ...composed.profile.patches,
    ...composed.homePatches,
    ...composed.overlays,
  ]
}
Source snapshot note: Based on the local deepseek-harness-master repo; verified against apps/cli/src/profile-boot.ts, verified on 2026-08-13. Code blocks keep the original source text.

Source: empty-array root config and template comment at apps/cli/src/profile-boot.ts lines 60–64; why Home overrides Profile at packages/boot/app-boot/README.zh.md line 43; repeatable --patch at apps/cli/src/args.ts line 132; hot reload via same-package watchUserPatches.

Why it lasts

Layered override is a config-system universal: CSS cascade, systemd drop-in dirs, editor user settings over defaults — same structure. Whenever one product is edited by release, team, individual, and single-command roles at once, layers must separate, or upgrades and rollbacks have nowhere to start. Rewrite the whole harness in another language and these four layers are still these four layers.

Design idea 2 · Hit means whole-entry replace

What problem it solves

Stacking still needs one key decision: what when two patch layers hit the same entry? The intuitive answer is deep-merge — field-level merge, upper layer writes only what it wants to change, other fields keep automatically. Easy to write.

Easy has two costs. First, merge can’t express delete: if a lower layer set a field and the upper wants it gone, merge has no such action — you can only overwrite with another value. Second, the final result matches no file’s literal content; debugging config means running every layer’s merge algorithm in your head before you know what’s actually in effect.

What the idea is

DSH chose whole-entry replace. After a patch hits a target by id, every top-level key in the patch except id is assigned over. config is one top-level key, so the old config object is wholly replaced — none of its fields survive. Want to change one field? You still restate the fields to keep. Official docs call this a known limitation, in so many words: “profile overrides must restate bundle fields that should be preserved.”

That yields the easiest counterintuitive trap — the scene layer 3 demos: Home writes only model, and provider and temperature set by the two layers below get wiped, with no warning, just a wrong result. Three behavior edges to remember: a patch pointing at a missing id warns but doesn’t error — one stderr line, then skip; an empty or comment-only patch file throws, because the parse result isn’t a list; to make a layer do nothing, write [].

Replace semantics pays off in predictability. There’s one copy of this algorithm in the whole repo: mount uses it, offline compose for dsh --dump-config uses it — dump and real boot cannot drift. Inputs are never mutated; results are always deep copies, so when hot-reload drops a patch it truly restores — earlier values aren’t baked into a cache. There’s also a what-you-see-is-what-you-edit map: docs/config-catalog.zh.md, a 3152-line generated doc listing every loadable package’s config types as-is. Want to know which keys a patch can write? Check that catalog.

What dump shows is what editing config gets you.

Source: replace semantics (top-level keys assigned one by one) at vendor/include/src/index.ts lines 110–124; the single-algorithm and immutable-input promise in the same file’s JSDoc lines 43–52; official whole-entry replace note at packages/boot/app-boot/README.zh.md line 60.

Why it lasts

It’s an old declarative-config choice: merge’s convenience vs replace’s readability. Replace gives each entry a unique last author — when something breaks, find the last patch and its literal content is what’s in effect; the cost is restating when you write. DSH scored the ledger toward predictability. Any config system that allows multi-party stacking and expects users to self-debug faces this question — independent of implementation language.

Side-by-side · How three products layer config

DeepSeek Harness

Plugin-entry level · whole-entry replace

The unit of a layer is a plugin entry: one patch replaces a whole config. Four layers from Bundle to --patch in fixed order; root config is an empty array; every line traces to a layer.

Swapping a deep capability means swapping one entry: replace the compaction plugin’s config wholesale, or even insert a third-party implementation.

Claude Code

Settings-field level · five sources

SETTING_SOURCES lists five layers: userSettings, projectSettings, localSettings, flagSettings, policySettings — later is larger (settings/constants.ts lines 7–22). Admin policy always sits on top, and some fields only take effect from managed sources.

What gets layered is settings fields — behavior parameters. The plugin tree itself isn’t on the config table.

Grok Build

TOML field level · deep merge

xai-grok-config recursively merges with deep_merge_toml (loader.rs lines 415–426): tables merge, arrays replace, upper fields win. Layer order is system_managed, managed, user; requirements and MDM managed layers sit on top last (loader.rs lines 234–248).

It picked the deep-merge contrast from the DSH demo: change one field without restating, but the upper layer can’t delete a lower-layer field.

Two comparison foci. First, merge semantics: Grok and Claude Code both do field-level merge — easy to write; DSH does entry-level replace — verbose to write, in exchange for dump matching file literals. Second, what gets layered: those two layer settings fields; DSH layers the plugin tree itself, so how deep users can reach differs — one insert patch can wire a third-party compaction into the main loop, with no counterpart in the other two config systems. Claude Code also has what DSH lacks: an admin policy layer and locked fields that only apply from managed sources — DSH has no equivalent today, based on public materials.

Classroom Exercise
01

Hand-trace a two-layer conflict

Profile layer writes id: conversation-model, config: { provider: deepseek, model: v3.2, temperature: 0.2 }; Home writes id: conversation-model, config: { temperature: 0 }.

Q1: Under whole-entry replace, write the final entry’s config — which fields vanish, and will any warning tip you off?

Q2: Swap which layer each patch sits in — what does the result become?

Q3: Using the dsh --dump-config idea, explain how to verify your answer without starting the product.

Takeaway: DSH config is four patch layers brushed onto an empty array — Bundle, Profile, Home, --patch, later wins. When a patch hits the same id it replaces the whole config, no field merge; fields you want to keep must be restated. The same algorithm drives mount and dump, so what --dump-config shows is what editing config gets you.