dshkit

dsh CLI reference: every subcommand, flag and entry mode

The complete dsh launcher reference — --profile, --port, --patch, --dump-config, --dump-default-config, the plugin subcommand, headless job invocation, $DSH_HOME paths, argument order and exit codes.

Updated 2026-08-133 min
Short answer

dsh is a launcher. It parses its own flags — --profile, --patch, --dump-config, --dump-default-config, --help — then hands everything from the first unrecognised token onward to the app it boots. dsh web is shorthand for --profile web; dsh --profile headless "job" runs one session and exits; dsh plugin --profile <name> forwards to pnpm.

dsh is a launcher, not an application. It resolves a profile, composes a plugin tree, and boots whatever that composition describes. Nearly every confusing behaviour in the CLI follows from that one fact — most of all the argument-order rule.

Entry modes

ModeInvocationWhat happens
Profiledsh --profile <name>Boots the named profile under $DSH_HOME/profiles/<name>
Webdsh webEquivalent to --profile web
Headlessdsh --profile headless "job"Executes one session, outputs the result, terminates
Plugin managerdsh plugin --profile <name> <pnpm args>Manages a profile's plugins by forwarding to pnpm

Launcher flags

FlagPurpose
--profile <name>Which profile to load
--patch <file>Applies a configuration overlay — the last and highest-priority patch layer
--dump-configPrints the current composed configuration
--dump-default-configPrints the composed tree without initialising the profile
--helpThe launcher's own help output
--portPassed through to the booted app, not consumed by the launcher

--port is in this table with a caveat because it is the flag people most often get wrong. It belongs to the app, which is why it must appear after the launcher's flags.

The argument-order rule

The launcher parses its own flags, and the first token it does not recognise begins the application's argument sequence.

dsh --profile web --port 3100      # works

Everything before --port is launcher vocabulary; --port 3100 goes to the web app. Reverse them and the launcher stops parsing at --port, so --profile web is handed to the app as arguments it does not understand.

In scripts this matters more than interactively, because an empty variable expansion can shift where that boundary lands. Quote your variables.

Exit codes

Invalid commands, options belonging to another mode, configuration errors and boot failures all exit nonzero.

That is a stronger guarantee than it sounds. The failure you fear from an automated agent is not a crash — it is a silent success that did nothing. A launcher that fails loudly on a misplaced flag means a broken pipeline announces itself on run one.

if ! dsh --profile headless "$JOB"; then
  echo "harness run failed" >&2
  exit 1
fi

Paths and environment

PathContents
$DSH_HOMEThe harness home directory — the root of your own overrides
$DSH_HOME/profiles/<name>One profile: its bundle list, its out-of-tree plugins, its patch file
$DSH_HOME/profiles/<name>/cordis.patch.ymlProfile-level patch layer
$DSH_HOME/cordis.patch.ymlHome-level patch layer — applies to every profile, and beats the profile's own
$DSH_HOME/settings.yamlCustom providers and model overrides
$DSH_HOME/.credentials.yamlCredentials, kept separate so settings.yaml stays shareable

Inspecting a composition

dsh --profile web --dump-config

Prints every row that patches can target, after all four layers have applied — bundles, the profile patch file, the home patch file, then --patch. When behaviour and expectation disagree, this output is the arbiter, not your source files.

dsh --profile web --dump-default-config

Prints the composed tree without initialising the profile. Diffing the two is the fastest way to see exactly what your own layers changed.

Managing plugins

dsh plugin --profile web add some-cordis-plugin
dsh plugin --profile web remove some-cordis-plugin

The subcommand forwards its arguments to pnpm, so the full pnpm vocabulary works — version specifiers included. Plugins install into the profile's own node_modules, which is why two profiles can hold incompatible plugin sets without interfering.

Bundles resolve from the dsh installation first, then from the profile's node_modules.

A short cheat sheet

# Interactive browser agent
dsh web
 
# Same thing, explicit, on a different port
dsh --profile web --port 3100
 
# One-shot run for CI
dsh --profile headless "summarise the failing tests"
 
# What am I actually running?
dsh --profile web --dump-config
 
# What would a clean profile look like?
dsh --profile web --dump-default-config
 
# Try a change without touching any file
dsh --profile web --patch ./experiments/strict-approval.yml
 
# Install a plugin into one profile only
dsh plugin --profile web add some-cordis-plugin

Frequently asked

Why does the order of flags matter?

The launcher parses its own flags first and treats the first token it does not recognise as the start of the app's argument list. Launcher flags such as --profile must come before app flags such as --port.

What is the difference between --dump-config and --dump-default-config?

--dump-config shows the current configuration state, after your patch layers. --dump-default-config displays the composed tree without initialising the profile — the clean baseline. Diff them to see exactly what you changed.

Does dsh exit nonzero on failure?

Yes. Invalid commands, options belonging to another mode, configuration errors and boot failures all exit nonzero, which is what makes dsh safe to wrap in a script.

Keep reading