This guide walks through the two nouns you meet in dsh every day — profile and bundle — and the dsh plugin command that ties them together. Every claim below is grounded in the upstream CLI reference (apps/cli/reference/README.md), architecture overview (docs/architecture.md), or package-and-install tutorial (docs/user/develop/basic/publish.md).
What a profile is
Per the architecture overview, a profile is a named composition stored in the Harness home. It lives at $DSH_HOME/profiles/<name> and carries three things:
- the bundles it stacks, listed in order in its
dsh.profile.bundlesmanifest - any out-of-tree plugins it installs (managed by pnpm in the profile directory)
- the user’s own
cordis.patch.yml— overrides that apply on top of every bundle
Two profiles ship as templates that auto-initialize on first use: web (base + web-app) and headless (base + headless). Any other name fails loud with a hint to run dsh plugin --profile <name> add <package>.
How dsh --profile <name> boots
Per the CLI reference, dsh --profile <name> boots the profile at $DSH_HOME/profiles/<name>. The effective tree is composed over an empty root by applying, in this order:
- each bundle patch named in the profile manifest’s
dsh.profile.bundleslist, in list order - the profile’s own
cordis.patch.yml - the home-level
$DSH_HOME/cordis.patch.yml(machine-local preferences shared by every profile, so it outranks the per-profile layer) - each
--patch <path>overlay in argv order
Later layers win per row, and a patch replaces a row’s entire config value rather than deep-merging keys. dsh web is a hardcoded alias for --profile web; the rest of the argv is handed to the booted profile verbatim through ctx.cmdlineArgs.
dsh plugin --profile <name> ... for managing bundles
Per the CLI reference, dsh plugin --profile <name> <args...>:
- initializes the profile when missing (shipped template, or
@deepseek-ai/dsh-basealone for other names) - then forwards
<args...>to pnpm with the profile directory as the working directory
That second point is the practical one: every pnpm verb works — add, remove, why, update, plus everything else. The canonical moves are:
dsh plugin --profile <name> add @scope/pkg
dsh plugin --profile <name> remove @scope/pkg
dsh plugin --profile <name> update
dsh does not invent its own subcommand surface; if a verb is not in pnpm, it does not work here either.
Two important behavioral details
Per the CLI reference:
- Bundle membership requires a profile restart. A running profile keeps the bundle set from its current start; ordinary edits to the profile’s or home-level
cordis.patch.ymlare reapplied transactionally (hot reload), but adding, removing, or updating a bundle is a startup boundary. - Git-hosted source-only plugins hit a pnpm ≥10
allowBuildsblock. A TypeScript package installed from Git arrives without its builtlib/; copy the package key pnpm printed into the profile’spnpm-workspace.yamlunderallowBuildsand re-run, or distribute prebuilt artifacts via npm orpnpm packtarballs instead.
Inspect without booting
Per the CLI reference:
dsh --profile web --dump-default-config
dsh --profile web --patch ./extra.yml --dump-config
--dump-default-config prints only the bundle layers; --dump-config adds the profile’s cordis.patch.yml, the home-level patch, and --patch overlays. Both print comments naming the file that supplied each row. !!js expressions remain unevaluated; a dump never runs app command-line providers, so it shows the composed tree before any app argument is parsed.
Where this fits with the rest of the site
- Plugin 101 — the build-side companion: how to write a plugin and package it as a bundle
- Quick start — the prerequisite launch path
- What is DeepSeek Harness? — the architecture primer
- What is Cordis? — the context model that plugins extend
- The map — every plugin referenced from this guide is indexed there
- Stats — live build-time counts referenced in the methodology note