DSH plugin shell and subprocess seams: ShellExecRequest

Plugin DevelopmentPublished 2026-10-17Author: DeepSeek Plugin Market
DeepSeek HarnessDSH pluginShellExecRequestDSH_*subprocesssandbox
How DSH plugin execution works: requests resolve into specs, DSH_* never inherits, sandbox denial differs from runner failure, stdio is explicit.

DSH plugin execution has two layers: the shell seam for "run one command," and the subprocess seam for "start a process and own its streams." The first gives you ctx.shell and resolve(), which turns optional request fields into a required spec; the second gives you fully explicit stdio dispositions and the managed DSH_* namespace. They share one vocabulary — DSH_* naming, credential scrubbing and the CollectedOutput shape belong to the subprocess seam and are re-exported by shell. That split explains why stdin, env and stdoutMaxBytes are open to in-process plugins but not exposed by the model-facing bash tool.

DSH plugin ShellExecRequest: requests, specs and results

Bottom line: requests are optional, specs are required, and resolve() is the only conversion point and the only source of caps.

  1. Request shape: only command is required; workdir, timeoutMs, onExpiry and stdoutMaxBytes are optional.
  2. Spec shape: all of those fields are required and filled by resolve() from implementation configuration.
  3. Expiry policy: onExpiry defaults to kill; with none no timer is armed and timeoutMs is echoed only.
  4. Orthogonal results: exitCode, signal, timedOut and aborted are independent; timedOut and aborted are mutually exclusive by first cause.
  5. Output shape: stdout and stderr are CollectedOutput, keeping the tail when truncated and optionally spilling the full stream.
  6. Handles are not jobs: the seam returns process handles; background job ids, ownership and control belong to the jobs subsystem.

DSH plugin DSH_* environment: why inheritance is forbidden

Bottom line: DSH_* are Harness facts, not caller input; implementations clear inherited values before merging the snapshot.

  1. Collection: the model-facing bash tool collects them through ctx.shellEnv and passes ShellExecRequest.dshEnv.
  2. Clear order: the subprocess service removes inherited DSH_* names before merging this snapshot.
  3. Why: an unavailable current fact must not inherit a stale value, and caller env must not displace managed values.
  4. Ordinary environment: env merges after credential scrubbing, and an explicit undefined tombstone deletes an existing value.
  5. Verify: print a DSH_* variable inside the command; even if the parent set an old value, it must reflect this snapshot.

DSH plugin sandbox facts: denied versus runnerFailed

Bottom line: denied is a policy refusal, runnerFailed means the executor never started — handle them differently.

FieldMeaningHandling
modeThe mode the command actually ran underCheck whether it is looser than expected
deniedThe sandbox refused a file operationA policy refusal
enforcementHow completely the runner enforced the modeDetect degradation
runnerFailedThe runner failed before the command ranForeground throws SANDBOX_UNAVAILABLE
  1. Foreground versus background: foreground throws on runnerFailed; a finished background process reports through facts only.
  2. Mode visibility: the model learns the effective mode only when a denial marks it.
  3. One-off relaxation needs approval: sandbox_permissions with a justification requests a strictly looser retry, and ctx.approval must approve that exact call first.
  4. Modes govern file effects only: process constraints belong to the sandbox seam.
  5. Common misread: a non-zero exit is not automatically a sandbox denial; check denied.

DSH plugin subprocess seam: explicit stdio and no shell parsing

Bottom line: there are no defaults — every disposition, cap and directory is on the spec, and argv never passes through a shell.

ts
interface SubprocessStdio {
  stdin: 'ignore' | 'pipe' | { data: string }
  stdout: 'pipe' | 'inherit' | SubprocessCollect
  stderr: 'pipe' | 'inherit' | SubprocessCollect
  control?: 'pipe'
}
  1. stdin three ways: ignore uses /dev/null, pipe exposes the stream for ongoing writes, { data } writes once and closes.
  2. Output three ways: pipe gives the raw Readable for protocol decoding, inherit passes the parent descriptor through, SubprocessCollect buffers boundedly.
  3. Collect and spill: maxBytes caps memory and keeps the tail; spill.maxBytes makes the complete stream recoverable, discarding an incomplete spill on overflow.
  4. Control channel: declare control: 'pipe' for a byte-mode duplex channel; without it none exists.
  5. Executable resolution: resolveExecutable(command, env?, signal?) validates absolute paths or resolves bare names through the provider's cleaned PATH.
  6. Results carry exit facts only: classification and semantics belong to consumers.

DSH plugin consumers: one seam, four stream dispositions

Bottom line: collect for batch output, pipe for protocol framing, inherit for diagnostics, terminal primitives for PTY.

  1. Bash family: collect mode with bounded batch output.
  2. LSP: raw protocol pipes with JSON-RPC framing.
  3. PTY backend: terminal process primitives.
  4. ACP subagents: ndjson over a pipe with stderr inherited.
  5. Selection rule: pipe for structured parsing, inherit for human diagnostics, collect for display.
  6. Diagnostic tails may skip spill: omit spill to keep only the in-memory tail.

DSH plugin execution troubleshooting

Bottom line: separate missing spec fields, wrong environment, sandbox refusal and the wrong stream disposition.

  1. Missing-field errors — you passed a request where a spec was required; call resolve().
  2. Stale DSH_* values — do not inherit; pass the dshEnv snapshot.
  3. Command refused — check denied and mode before doubting the command.
  4. Foreground throws SANDBOX_UNAVAILABLE — runnerFailed; the runner never started.
  5. Output missing its head — truncation keeps the tail; enable spill for the complete stream.
  6. Repair status, stated honestly: request/spec split, DSH_* rules, sandbox facts and stdio dispositions follow the official Shell executor, Subprocess and Sandbox pages; confirm signatures against their Cordis APIs and your version.

Sources: Shell executor, Subprocess, Sandbox.

FAQ

Why does a DSH plugin have to call resolve() before executing a shell command?

The DSH plugin shell seam separates the model-facing request from the executor's spec: workdir and timeoutMs are optional in the request and resolve fills them from configuration with caps. Tools must resolve first, because the required spec is what actually executes.

Why can DSH_* variables not be inherited from the parent in a DSH plugin?

DSH_* are DeepSeek Harness-owned child-process facts, and implementations discard inherited DSH_* names before merging this execution's snapshot. Otherwise an unavailable fact would inherit a stale value and caller env entries could displace managed ones.

What is the difference between DSH plugin sandbox denied and runnerFailed?

In DSH plugin shell results, denied means the sandbox refused a file operation, while runnerFailed means the runner failed before the command could run. Foreground execution throws SANDBOX_UNAVAILABLE on runnerFailed; finished background processes report through their facts channel.

Where does truncated DSH plugin subprocess stdout go?

DSH plugin CollectedOutput keeps the tail of the stream in text when truncated, and the complete stream goes to a private file exposed through spillPath. Keeping the spill file is the consumer's choice; diagnostic tails can stay in memory only.

Does the DSH plugin subprocess seam apply default stdio settings?

No: the DSH plugin subprocess seam applies no defaults, so stdin, stdout and stderr dispositions, caps and directories are all explicit. argv is never interpreted by a shell either, avoiding implicit parsing and injection surface.

Related Terms

ShellExecRequest
ShellExecRequest is the model- or plugin-facing execution request where command is required and workdir, timeoutMs, onExpiry and stdoutMaxBytes are optional. It must be resolved into a ShellExecSpec before execution.— https://deepseek-harness.github.io/deepseek-harness/reference/subsystems/shell
CollectedOutput
CollectedOutput is one captured output stream: text keeps the tail when truncated, truncated marks dropped bytes, and spillPath points at a file holding the complete stream.— https://deepseek-harness.github.io/deepseek-harness/reference/subsystems/subprocess
DshEnvironment
DshEnvironment is the set of DeepSeek Harness-owned DSH_* variables for one child-process execution, keyed inside the managed namespace. Implementations clear inherited DSH_* names before merging this snapshot.— https://deepseek-harness.github.io/deepseek-harness/reference/subsystems/subprocess
SubprocessCollect
SubprocessCollect is a subprocess output disposition that buffers boundedly in memory by maxBytes, keeps the tail on overflow, and optionally spills the complete stream to a capped file.— https://deepseek-harness.github.io/deepseek-harness/reference/subsystems/subprocess

Sources