Running DSH plugin commands in scripts and CI safely
Putting dsh plugin commands into scripts and CI comes down to three things: use commands that exit when done, judge success by exit code rather than output, and pre-supply answers for anything that would ask for confirmation. dsh plugin --profile <name> add / remove / list and dsh --profile <name> --dump-config are all one-shot commands; the CLI returns non-zero for invalid commands, misused arguments, configuration errors, and startup failures, so set -e in a script catches the vast majority of failures (source).
Which dsh plugin commands belong in scripts, and which will hang
The dividing line is whether they exit when done: one-shot commands fit a pipeline, resident processes do not (source). Run through the common commands by that standard:
| Command | Safe in scripts | Notes |
|---|---|---|
dsh plugin --profile <name> add <target> | Yes | Exits after installing; the exit code carries the result |
dsh plugin --profile <name> remove <package> | Yes | Exits after removing; safe to rerun |
dsh plugin --profile <name> list | Yes | One-shot output, good for state assertions |
dsh --profile <name> --dump-config | Yes | Does not start the app; prints the merged config tree |
dsh web | No | Resident, holds the terminal, a service process |
There is another form of "hanging": the command is not stuck, it is waiting for confirmation. pnpm stops for an answer in two situations — the purge confirmation before deleting node_modules, and build script authorization for git dependencies. Locally you tap Enter without thinking; in CI you wait until the timeout. Both confirmations must be answered in advance via an argument or a config file, as the non-interactive form below shows.
Judge DSH plugin success by exit code, not by grepping the output
The success contract for dsh plugin commands is the exit code, not the output text: output changes with version and locale, exit codes do not (source). Let failures abort directly:
set -euo pipefail
dsh plugin --profile web add "<package>"
dsh --profile web --dump-config | grep -q "# =="
That second line is the real acceptance check: a successful add only means the dependency was installed, whereas a # == <package> line in --dump-config means the config layer entered the bundle tree (source). Keeping the two steps separate lets the log distinguish "could not install" from "installed but not live" at a glance — the latter usually means the package does not declare dsh.bundle.
When you need branching, use if rather than parsing text:
if dsh plugin --profile web list | grep -q "<package>"; then
echo "already installed"
else
dsh plugin --profile web add "<package>"
fi
Do not treat grep "error" as a failure test: the word can appear in pnpm warning text, while the real failure is already expressed by a non-zero exit code.
Idempotent DSH plugin reruns: what to do when it is already installed
"Rerun toward the desired state" beats "rerun the action": read the current state first, then decide whether to act (source). Two practices:
- Make the state query the script's first step:
dsh plugin --profile web list
Its output comes from the pnpm inside the profile directory and shows package names and versions. Using it as the basis makes the script's logic explicit, instead of depending on the implicit behavior of "what happens if I add again".
- When you need a fixed version, put the version into the target. The worst thing to write in a script is "latest" — working today and failing tomorrow because upstream published a new release. Pin the target to a specific version or commit so reruns stay stable:
# npm package: with a version
dsh plugin --profile web add "<package>@<version>"
# GitHub source: lock the commit
dsh plugin --profile web add github:owner/repo#<sha>
Install it by hand once locally before writing the script: clicking install for the same package in DSH Plugin Hub verifies that "the plugin itself installs and is actually live", and once confirmed you can move the equivalent command into the script. Skip this and it becomes hard to tell whether the plugin or the script is at fault.
The minimal working form for dsh plugin in CI and containers
Three prerequisites in a container: a recent enough Node version, pnpm on PATH, and a writable $DSH_HOME; once met, one command is enough (source). The minimal snippet:
npm install -g pnpm
export CI=true
export DSH_HOME="${HOME}/.dsh"
dsh plugin --profile web add --config.confirm-modules-purge=false "<package>"
dsh --profile web --dump-config | grep -q "# =="
The points, one by one:
CI=trueputs pnpm on the non-interactive branch, so anything that would have prompted is handled non-interactively;--config.confirm-modules-purge=falsedisables the confirmation before deletingnode_modules, avoiding a stall while clearing caches;pnpmmust be on PATH — with no pnpm in the container image, the command fails in the shape ofpnpm failed in profile directory; how to diagnose that is in how to fix "pnpm failed in profile directory";- On restricted networks the registry must point at an internal source, or the build stage stalls fetching packages — see how to install dsh on an intranet or offline;
- Authorize git dependencies in advance: write pnpm's exact printed package key into that profile's
pnpm-workspace.yaml:
allowBuilds:
<package>: true
This step pre-authorizes install-time code execution, so use it only for packages whose source you trust (source).
DSH plugin scripting notes
In one line: pick commands that exit, judge by exit code, pre-answer confirmations, and pin versions. Seven reminders:
- Resident commands do not go into a pipeline: processes like
dsh webhold the terminal and only suit background services; - Accept with
--dump-config: theaddexit code only proves the dependency was installed; the config layer must be confirmed in the bundle tree; - Do not parse output text: wording and versions both change, only the exit code is a stable contract;
- Always pin the version: write
<package>@<version>or#<sha>so reruns do not drift with upstream; - Supply confirmations in advance: disable purge with an argument and authorize build scripts with
allowBuilds, rather than letting CI wait for an Enter key; $DSH_HOMEmust be writable: in a container the default path may land on a read-only layer, so point it explicitly at a writable volume when needed and keep it consistent across steps;- Back up the profile before upgrading:
cp -r "$DSH_HOME/profiles/<name>" "$DSH_HOME/profiles/<name>.bak"so a script that breaks something can be restored directory by directory.
Once the commands are composed for non-interactive use, what remains is argument detail; if you want a full command reference sorted by feature or alphabetically, that is a different kind of article — this one only covers "getting it to run unattended".

Sources: dsh CLI README, DeepSeek Harness docs - Packaging and installing a plugin
FAQ
The dsh plugin commands suited to scripts are the ones that exit after finishing their job: dsh plugin --profile <name> add / remove / list and dsh --profile <name> --dump-config all fall into this class and return as soon as they are done. What does not fit are resident commands — dsh web holds the terminal and never returns, so it can only be used as a background service and cannot be awaited in a pipeline.
Judging dsh plugin success in CI relies on exit codes: the CLI returns a non-zero status for invalid commands, misused arguments, configuration errors, and startup failures, so set -e or if ! cmd in a script catches them directly. Grepping the output for keywords is fragile — the wording changes between versions, and differs between English and Chinese environments, whereas the exit code is the stable contract.
dsh plugin add does not fail just because the package already exists; it behaves as an in-place install, so rerunning toward a desired state is naturally close to idempotent. To be safer, run dsh plugin --profile <name> list first to read the current state and let that output decide whether add should run, writing the decision explicitly into the script rather than relying on pnpm's default behavior.
In CI or a container, a DSH plugin command hanging on a prompt usually means pnpm is waiting for interactive confirmation. The two common spots are the purge confirmation before deleting node_modules and the build script authorization for git dependencies: the former is silenced with --config.confirm-modules-purge=false or CI=true to take the non-interactive branch, and the latter by writing pnpm's printed package key into that profile's pnpm-workspace.yaml allowBuilds so authorization becomes configuration rather than a question.
Three prerequisites for running dsh plugin in a container: a recent enough Node version, pnpm on PATH, and a writable $DSH_HOME. The minimal working form is a single line, CI=true dsh plugin --profile web add <package>, preceded by npm install -g pnpm to ensure pnpm exists and, when needed, a registry pointing at an internal source; afterwards run dsh --profile web --dump-config to assert the config layer was generated.
Related Terms
- non-interactive execution
- Non-interactive execution means a command runs to completion with nobody at the terminal: it waits for no input, raises no confirmation, and reports results only through its exit code. It is the precondition for putting dsh plugin commands into scripts and CI — anywhere a confirmation would appear, the answer must be supplied in advance by an argument or a config file.— dsh CLI README
- exit code
- An exit code is the integer status a process returns to its caller on termination, 0 meaning success and non-zero meaning failure. The CLI returns non-zero for invalid commands, misused arguments, configuration errors, and startup failures, making it the stable basis for judging dsh plugin commands in a script.— dsh CLI README
- idempotent rerun
- An idempotent rerun means running the same command repeatedly produces the same result as running it once. For dsh plugin the reliable approach is to read the current state with list first and then decide whether add should run, making the desired state — not whether the command was already run — the basis for the script's decision.— dsh CLI README
- allowBuilds
- allowBuilds is the build script allowlist written into that profile's pnpm-workspace.yaml. pnpm refuses to run build scripts of git dependencies by default; writing the package key into allowBuilds pre-authorizes it and keeps the install from stopping to ask you.— DeepSeek Harness docs - Packaging and installing a plugin
Sources
- dsh CLI README· deepseek-ai
- DeepSeek Harness docs - Packaging and installing a plugin· deepseek-harness