Skip to content
Agentic Control Plane

The dsh control model, explained

DeepSeek Harness shipped in developer preview in mid-August 2026, and its control story is worth studying even if you never run it — because dsh made architectural choices most established harnesses are still retrofitting. This page is the reference: what dsh ships natively for controlling tool execution, exactly how each mechanism behaves, and where its model ends.

This page covers dsh’s own controls. For wiring ACP into dsh, see the install guide; for what our plugin intercepts, the coverage matrix.

The one-pipeline architecture

The single most important fact about dsh: everything dispatches through one pipeline. dsh is built on Cordis — the harness’s own built-in tools are plugins on typed extension points, and so is anything you add. bash, pwsh, read/write/edit, glob/grep, web_fetch/web_search, subagent spawns, and the serialized sub-calls inside Code Mode all flow through the same dispatch path.

That sounds like plumbing, but it’s the property every control question reduces to. In harnesses that grew hooks after the fact, coverage is a per-tool negotiation — shell fires the hook, file edits maybe, MCP calls it depends. In dsh, if you stand on the pipeline, you see every call. There is no second door.

The full dispatch is a Cordis waterfall — tool/calltools/pre-executetools/executetools/post-executetool/result, with each listener calling next() to delegate — and MCP tools ride the same pipeline (exposed as mcp__<server>__<name>). The two points that matter for control:

Extension point Fires Can
tools/pre-execute before every tool call return allow / ask / deny; a deny skips the call with the reason recorded in the session log
tools/post-execute after every tool call inspect the result and replace its content before the model reads it

Both are ordinary Cordis plugin registrations — the same mechanism dsh’s own tools use, in-process, no subprocess or JSON-over-stdin dialect. The ask return feeds dsh’s native approval flow (ctx.approval) rather than inventing a parallel one.

The post-execute point deserves emphasis: it can rewrite what the model sees, not just observe it. Most harnesses’ post-hooks are observational — by the time you’ve spotted the leaked credential in a tool result, the model has already read it. dsh is one of two harnesses we’ve integrated where inline rewrite works (see the coverage matrix for the comparison).

The approval service

Approvals in dsh are a composition decision, not a setting. A profile that mounts an approval service gets interactive prompts: an ask — from dsh’s own logic or from any plugin — surfaces to the human, and the answer resolves the call.

The interesting part is what happens when no approval service is mounted:

Asks deny themselves. In a headless composition, dsh resolves ask to deny by design. An unattended agent cannot self-approve.

This is the single best native control decision we’ve seen a harness ship in 2026. Every harness has some answer to “what happens to an approval prompt when nobody’s there” — usually an undocumented timeout, occasionally a silent auto-approve. dsh made the unattended resolution explicit, fail-closed, and structural: the deny doesn’t come from a rule you remembered to write; it comes from the absence of anyone who could say yes. The same posture extends to delegation — the subagent bridge auto-answers child prompts with reject by default. We’ve written about why this is the only honest resolution in the empty-chair test.

The sandbox and the presets

dsh ships a native sandbox, and its default is the strictest mode. ctx.sandbox wraps the command line before spawn; @deepseek-ai/dsh-sandbox-policy resolves the per-session mode:

  • read-only — the default, documented as the fail-safe.
  • workspace-write — writes confined to workspaceRoot (default: the cwd).
  • danger-full-access — the name says it.

The local backend runs commands through a bwrap-compatible runner; a remote E2B backend exists for disposable execution. The ecosystem’s own guidance is worth repeating: treat the sandbox as filesystem scoping, not a general security boundary.

The official dsh-permission-presets plugin pairs sandbox mode with approval policy as named presets — workspace-write (sandbox workspace-write + approval ask) and danger-full-access (sandbox open + approval never). That second preset is dsh’s honest version of a yolo flag: it exists, it’s named like a warning, and selecting it is a visible profile decision rather than a forgotten CLI flag.

Core ships no rule language — that’s the plugin ecosystem’s job, and it’s already forming: community dsh-tool-policy does deny-by-default ordered rules (first match wins, anchored tool patterns, JSON-Pointer argument matchers, ask routed into ctx.approval), and dsh-auto-review puts an LLM reviewer on the approval answerer chain, fail-closed. Three days after launch there were 300+ plugins under the dsh-plugin topic; policy plugins are among the first things the ecosystem built, which tells you something about what users reach for.

Configuration and profiles

dsh’s unit of configuration is the profile — a composition of plugins declared in package.json ("dsh.profile.bundles") and adjusted via cordis.patch.yml overlays. Controls follow the same shape:

  • Adding or removing an interception plugin is a profile edit, verifiable with dsh --profile <name> --dump-config.
  • --patch overlays adjust a plugin row’s config by file path — useful for per-machine overrides — but cannot change a row’s name, which limits what a stray patch file can substitute.
  • There is no global rules file, no allowlist syntax, no per-command patterns as shipped. Policy is expected to arrive as a plugin on the pipeline.

Sharp edges we’ve hit in the preview, recorded here because they affect control coverage: profile-installed plugins don’t resolve under a bare npx launch (ESM resolves from the npx cache — launch from an installed profile instead), and dsh needs Node 22; Node 20 fails boot cryptically. Preview means breaking changes are promised — verify your interception plugin actually mounted (--dump-config) after any dsh upgrade.

The session log

dsh’s native record is an append-only SessionEvent log with a runtime-enforced invariant the docs state in four words: model-visible means logged. Every turn, step, and tool event — including deny reasons — lands in the durable log, and the invariant means there is no code path where the model saw something the log didn’t capture. That’s the strongest native audit story of any harness we’ve surveyed; most ship a transcript and hope.

Know what it is and isn’t:

  • It’s still the run’s own account, written by the harness on the machine the agent ran on. For attended use that’s plenty. For unattended use, the machine that ran the agent is the wrong place for the only copy of the evidence — a compromised or wedged run takes its witness with it. (OTLP telemetry export exists via dsh-session-telemetry-otel, but it defaults to DISABLED.)
  • It’s an event log, not a decision ledger. It records that a call ran or was denied; it doesn’t record what was evaluated, against which policy version, on whose authority. “What did the agent do” and “why was that allowed” are different questions, and only the first one has a native answer.

Escape hatches and the bridge

  • dsh’s yolo equivalent is the danger-full-access preset — approval never, sandbox open. To its credit it’s a named, visible profile decision rather than a CLI flag someone forgets in a script; the discipline is the profile review (--dump-config), not a flag audit.
  • The hooks-claude-code bridge (@deepseek-ai/dsh-hooks-claude-code) runs an unmodified Claude Code hooks.json inside dsh. Deny and ask are honored; input rewriting is not. It’s a compatibility surface, useful for porting, weaker than a native plugin — if your control layer supports dsh natively, use that.
  • One naming collision to avoid in config reviews: dsh’s packages/acp is Zed’s Agent Client Protocol, unrelated to the Agentic Control Plane.

Where the native model ends

What dsh gives you natively: complete interception coverage, a real approval flow with the correct headless posture, a sandbox that defaults to read-only, inline output rewrite, and the best native event log in the ecosystem. What it deliberately doesn’t ship:

  • No policy engine in core. Presets pair sandbox with approval posture; rules, patterns, and tiers are the plugin ecosystem’s job.
  • No cross-machine anything. Profiles, trajectories, and approvals are all per-composition, per-machine. Two developers’ dsh setups share nothing unless you build the sharing.
  • No independent record. The trajectory is authored by the same process it describes.

That’s not a criticism — it’s a coherent design: dsh ships the architecture of control and leaves the content of control to the ecosystem. It’s also exactly the seam a control plane fills: our plugin mounts policy on tools/pre-execute (allow/ask/deny from workspace rules, with tier-aware resolution), output scanning with inline rewrite on tools/post-execute, and writes every decision to a ledger off the machine. Same pipeline, same typed points — dsh’s own mechanism, carrying policy you can query next week.