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
| Mode | Invocation | What happens |
|---|---|---|
| Profile | dsh --profile <name> | Boots the named profile under $DSH_HOME/profiles/<name> |
| Web | dsh web | Equivalent to --profile web |
| Headless | dsh --profile headless "job" | Executes one session, outputs the result, terminates |
| Plugin manager | dsh plugin --profile <name> <pnpm args> | Manages a profile's plugins by forwarding to pnpm |
Launcher flags
| Flag | Purpose |
|---|---|
--profile <name> | Which profile to load |
--patch <file> | Applies a configuration overlay — the last and highest-priority patch layer |
--dump-config | Prints the current composed configuration |
--dump-default-config | Prints the composed tree without initialising the profile |
--help | The launcher's own help output |
--port | Passed 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 # worksEverything 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
fiPaths and environment
| Path | Contents |
|---|---|
$DSH_HOME | The 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.yml | Profile-level patch layer |
$DSH_HOME/cordis.patch.yml | Home-level patch layer — applies to every profile, and beats the profile's own |
$DSH_HOME/settings.yaml | Custom providers and model overrides |
$DSH_HOME/.credentials.yaml | Credentials, kept separate so settings.yaml stays shareable |
Inspecting a composition
dsh --profile web --dump-configPrints 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-configPrints 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-pluginThe 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-pluginFrequently 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.