dshseek

seek://guides/profiles-and-dsh-plugin

DSH Profiles and the dsh plugin Command

Aug 24, 2026Intermediate← All guides

TL;DR

A field guide to DSH's bundle/profile two-noun system and the dsh plugin command that ties them together. A profile is a directory under $DSH_HOME/profiles/<name> listing bundles it stacks; dsh plugin forwards to pnpm inside the profile, so every pnpm verb works. The shipped apps are web and headless profiles, with web also reachable through the dsh web alias.

Key points

  • Per the official CLI reference, `dsh --profile <name>` boots the profile at `$DSH_HOME/profiles/<name>`; `dsh web` is a hardcoded alias for `--profile web`
  • 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>`
  • Per the architecture overview, a profile lists bundles it stacks (its `dsh.profile.bundles` array), holds any out-of-tree plugins it installs, and keeps the user's own `cordis.patch.yml`
  • Per the CLI reference, `dsh plugin --profile <name> <args...>` initializes the profile if missing and then forwards `<args...>` to pnpm — `add`, `remove`, `why`, `update`, and every other pnpm verb work unchanged; pnpm must be on PATH
  • Per the CLI reference, the effective configuration composes over an empty root by applying: each bundle patch in profile order, then the profile's `cordis.patch.yml`, then the home-level `$DSH_HOME/cordis.patch.yml`, then each `--patch <path>` overlay in argv order; later layers win per row
  • Per the CLI reference, `dsh --profile web --dump-config` (and `--dump-default-config`) print the composed tree without booting — useful for inspecting what a profile will load
  • Per the CLI reference, a running profile keeps the bundle set from its current start — ordinary `cordis.patch.yml` edits take effect through hot reload, but adding, removing, or updating a bundle requires a profile restart

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.bundles manifest
  • 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:

  1. each bundle patch named in the profile manifest’s dsh.profile.bundles list, in list order
  2. the profile’s own cordis.patch.yml
  3. the home-level $DSH_HOME/cordis.patch.yml (machine-local preferences shared by every profile, so it outranks the per-profile layer)
  4. 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-base alone 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.yml are reapplied transactionally (hot reload), but adding, removing, or updating a bundle is a startup boundary.
  • Git-hosted source-only plugins hit a pnpm ≥10 allowBuilds block. A TypeScript package installed from Git arrives without its built lib/; copy the package key pnpm printed into the profile’s pnpm-workspace.yaml under allowBuilds and re-run, or distribute prebuilt artifacts via npm or pnpm pack tarballs 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

Official references