# DeepSeek Harness (dsh) Permissions & Control Model, Explained

How dsh's approval service, typed plugin interception points, headless ask-denies-itself posture, and trajectory log actually work — what each catches, what none of them record, and how to extend them.

# The dsh control model, explained

DeepSeek Harness shipped in developer preview [in mid-August 2026](https://github.com/deepseek-ai/deepseek-harness), 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](/integrations/dsh); for what our plugin intercepts, the [coverage matrix](/coverage). (dsh also ships an unrelated `acp` — Zed's Agent Client Protocol, for editors; [which ACP is which](/acp-vs-acp).)*

## 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/call` → `tools/pre-execute` → `tools/execute` → `tools/post-execute` → `tool/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](/coverage) 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](/blog/interactive-vs-autonomous-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](/integrations/dsh) 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.

## Frequently asked questions


