Skip to content

Profiles, Bundles & Patches — dsh Guide

dsh runs on Cordis. The official architecture doc puts it bluntly: “every part of the product is a plugin, including the model adapter, the tool registry, the session log, and the agent loop itself.” There is no privileged core to hook into. You extend dsh by adding a plugin alongside the ones already there.

Getting all those plugins composed into something that boots takes three concepts: bundles, profiles, and patches. This page covers what each one holds and the order they apply in, following the architecture doc and development guide.

Bundle — a distribution format holding a set of Cordis config rows plus the plugin code those rows mount. A bundle is a pre-assembled slice of functionality: model adapters, tools, persistence, an entire web app. What it inserts is ordinary config rows, which means anything it sets up can still be overridden by a layer that loads later.

Profile — a named, runnable composition, stored under $DSH_HOME/profiles/<name>. It records the bundles it stacks and their order, any out-of-tree plugins installed into it, and your own override file. Two profile templates ship with dsh: web for the browser UI, headless for one-shot runs with no server.

Patch — a YAML file that edits rows already loaded, addressing them by id. It can merge into a row’s config, switch a row off, or insert rows that weren’t there before. Patching is how you change a profile without forking the bundles underneath it.

Composition starts from an empty list and applies layers in this order (source: architecture.md):

  1. Each bundle in the profile’s listed order (e.g. dsh-base, then dsh-web-app).
  2. The profile’s own cordis.patch.yml.
  3. The home-level $DSH_HOME/cordis.patch.yml (applies to every profile).
  4. Any --patch overlay passed on the command line.

dsh-base comes first in every profile. It’s the layer carrying model adapters, tools, persistence, sandbox and approval policy, settings, credentials, and telemetry. On top of that, dsh-web-app adds the browser UI, or dsh-headless adds a one-shot runner with no server.

To see the tree your own machine boots, with every layer already applied:

Terminal window
dsh --profile web --dump-config

Any row that prints can be overridden by a patch of your own.

  • A profile directory ($DSH_HOME/profiles/<name>/) holds a package.json with a dsh.profile.bundles list, plus a cordis.patch.yml for your overrides.
  • A bundle declares itself the same way: its package.json has a dsh.bundle field pointing at its own patch file.
  • Bundles named in dsh.profile.bundles resolve from the dsh installation first, then from the profile’s own node_modules (where pnpm installs anything out-of-tree you’ve added via dsh plugin add).

The snippets below are trimmed down from dsh’s own headless-agent example and the dsh-headless bundle patch. Don’t paste them into a real profile. The row shapes are accurate; the contents are made up to be readable.

A bundle contributes ordinary rows, each with a stable id:

# a bundle's cordis.yml
- id: llm-deepseek
name: '@deepseek-ai/dsh-llm-deepseek'
config:
thinking: enabled
reasoningEffort: max
- id: system-prompt
name: '@deepseek-ai/dsh-system-prompt'
config:
persona: 'You are a coding agent.'

A patch on top addresses rows by that same id. A partial config merges into the existing row, disabled: true switches one off, and insert: appends rows that didn’t exist:

# a patch (profile's or home-level cordis.patch.yml)
- id: system-prompt
config:
persona: 'You are my custom assistant. Working directory: {{cwd}}.'
- id: some-optional-row
disabled: true
- insert:
- id: my-extra-tool
name: '@deepseek-ai/dsh-tool-my-extra-tool'

None of this requires touching the bundle’s source. The patch layer is the entire customization surface.