# DeepSeek Harness gets an ACP plugin on day 3 of its developer preview

dsh ships typed interception points that make control a first-class plugin concern: allow/ask/deny before every tool call, inline result rewriting after it, and a native approval flow for asks. Here's what the new dsh-plugin-acp does, what it caught in our own gateway on day one, and where dsh sits on the cross-harness coverage table.

*(ACP here is the Agentic Control Plane — the policy layer this site is about. If you searched `dsh acp` looking for the **Agent Client Protocol** — Zed's editor protocol, which dsh also ships as `@deepseek-ai/dsh-acp` — [two unrelated projects share the acronym](/acp-vs-acp); that page routes you to the right one.)*

[DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) — the agentic coding harness DeepSeek released into developer preview this week — now has ACP support via [@agenticcontrolplane/dsh](https://github.com/agentic-control-plane/dsh-acp-plugin) ([npm](https://www.npmjs.com/package/@agenticcontrolplane/dsh)). One command:

```bash
curl -sf https://agenticcontrolplane.com/install.sh | bash
```

The installer detects dsh, adds the plugin to every profile you have, signs you in once in the browser — free for individuals — and writes the key to `~/.acp/credentials`, which the plugin reads on its own. Prefer to wire it by hand? `dsh plugin --profile <your-profile> add @agenticcontrolplane/dsh`, then export `ACP_BEARER_TOKEN` with a key from [cloud.agenticcontrolplane.com](https://cloud.agenticcontrolplane.com/login?from=%2Fonboarding&surface=dsh).

From the next boot, every tool call dsh makes — `bash`, `read`, `write`, `edit`, `web_fetch`, subagent spawns, even the serialized sub-calls inside its Code Mode — gets policy-checked against your workspace before it runs, and every decision lands in your activity log with the reason attached.

dsh is three days into a developer preview that promises breaking changes, so treat this plugin the same way: early, tested, and tracking a fast-moving target. But the interception surface it plugs into is one of the best we've integrated, and that's worth writing about.

## Where dsh lands on the coverage table

*(The living version of this table, updated with every integration, is at [/coverage](/coverage).)*

We've shipped ACP plugins for a half-dozen harnesses now (plus OpenCode and OpenClaw, which follow the same wire contract). The question that matters for each one: when your policy says no, what actually stops?

<div class="acp-post-wide-table" markdown="1">

| Harness     | What the pre-call check intercepts                                | Can policy ask a human? | Can the result be rewritten inline? |
|-------------|-------------------------------------------------------------------|-------------------------|-------------------------------------|
| Claude Code | All native tools + MCP                                            | Yes (native `ask`)      | Yes                                 |
| Codex CLI   | Bash only; edits need the MCP supplement                          | No (`ask` maps to deny + dashboard link) | No                 |
| Cursor      | Most native tools                                                 | Yes                     | No                                  |
| Hermes      | Everything, in-process                                            | No (`block` only)       | No (post hook is observational)     |
| **dsh**     | **Everything — native tools, custom plugins, Code Mode sub-calls** | **Yes — `ask` feeds dsh's own approval prompt** | **Yes — post-execute can replace result content** |

</div>

dsh is the first harness since Claude Code to land in the rightmost column on all three. That's not an accident of our plugin; it's their architecture.

## Why dsh ended up here

dsh is built on Cordis, a plugin framework where the harness's own tools, policy, and UI are all plugins on typed extension points. The tool-execution pipeline has a `tools/pre-execute` waterfall that any plugin can join. A listener returns one of three typed decisions:

- `{ kind: 'allow' }` — the call runs
- `{ kind: 'deny', reason }` — the call is skipped and the model sees the reason
- `{ kind: 'ask', reason }` — dsh's own approval service prompts the human; no approval service mounted (headless), the ask becomes a deny

That last line deserves a pause. In a headless composition, dsh resolves `ask` to deny *by design* — an unattended agent cannot self-approve. The harness ships the fail-closed posture for unattended runs that we've been arguing for since the Codex integration. We didn't have to add it.

After the call, a `tools/post-execute` waterfall can accept, block, or *replace the result content* the model sees. That's the hook Claude Code has and Hermes doesn't: it's where server-side output scanning stops being observational and starts being able to act — strip a leaked credential, drop injected instructions from a fetched page — before the model reads it.

The plugin itself is ~190 lines of plain ESM. No build step, no dependencies — the same thin-wrapper rule as our other plugins: every transitive dep on a control-plane plugin is a credential-adjacent supply-chain target, and this one has zero.

## What the integration caught on day one

The first live test found a real gap — in our gateway, not in dsh.

Every harness spells its tools differently. Claude Code says `Bash`; dsh says `bash`; Gemini CLI says `run_shell_command`. Our gateway's name-specific layers — the hardline floor that unconditionally blocks things like recursive root deletes, the per-binary and per-domain command classification, the dotted policy tiers — matched the Claude Code spellings exactly. A shell command arriving as `bash` evaluated policy correctly at the generic tier, but skipped every layer that knew what a *shell* is.

The fix is one alias table where classification begins, so every harness dialect canonicalizes before the hardline floor, the classifiers, and the policy tiers see it ([#676](https://github.com/davidcrowe/gatewaystack-connect/issues/676), live in production the same day). One change, five harnesses covered, including ones we haven't met yet — and the recursive-root-delete-as-`bash` repro that opened this section now comes back `deny`, with the hardline reason attached.

The general lesson holds beyond our gateway: if your control layer keys on tool *names*, every new harness is a silent coverage gap until proven otherwise. Test with the harness's real spellings, not your reference harness's.

## What you'll see in the dashboard

A `dsh-plugin` client row on [cloud.agenticcontrolplane.com](https://cloud.agenticcontrolplane.com), with the same shape as every other harness: tool name, input preview, decision, reason, latency, session and workspace attribution. Denials say why in the model-facing reason *and* in the log row. dsh's own append-only session log independently records the same calls — its Trajectory view and your ACP activity log are two witnesses to one history, which is exactly what you want when you're reconstructing what an agent did.

Fail posture is the standard one: interactive sessions fail open with a loud `[ACP] ⚠ UNGOVERNED` warning and a lapse-log entry if the gateway is unreachable; unattended tiers fail closed. Policy denies are unaffected — the posture only covers the inability to ask.

## Three real gotchas to know about

**dsh has a `packages/acp` that is not this.** It's [Zed's Agent Client Protocol](https://agentclientprotocol.com) — an editor-integration protocol that happens to share the acronym. If you're grepping the dsh source, `dsh-acp` the package is theirs; `dsh-plugin-acp` is this plugin. The full map of which ACP is which — including how the two compose in one dsh session — is at [/acp-vs-acp](/acp-vs-acp).

**Node 22 is required.** dsh's own packages use `Promise.withResolvers` and zstd streams from `node:zlib`. Under Node 20 the harness fails at boot with errors that don't say "your Node is too old." `fnm install 22` first.

**Profile-installed plugins don't resolve under a bare `npx` launch (rc.6).** `dsh plugin add` links the package into the profile, but an npx-run dsh resolves bare package names from the npx cache and won't find it. A `--patch` overlay that loads the plugin by file path works everywhere. Preview-grade sharp edge; expect it to change.

## What this means for the harness landscape

When dsh hit the front page this week, the top comment wasn't about the model — it was about the append-only trajectory log. Developers are telling harness authors, loudly, that they want to see exactly what their agent did.

Seeing is half of it. A trajectory tells you what happened; it can't stop what shouldn't. The interesting thing about dsh is that its authors built for both halves: the same typed extension points that feed its log let a plugin decide, before each call, whether it runs at all — with a human in the loop when policy says so, on by default, for every tool.

If you're building a harness and want it controllable: that's the spec. One pipeline every tool dispatch flows through, a typed pre-call decision with allow/ask/deny, a post-call hook that can rewrite what the model sees, and deny-by-default for asks nobody can answer. dsh shipped all four in a developer preview.

## Try it

```bash
fnm install 22 && fnm use 22   # dsh itself needs Node 22
curl -sf https://agenticcontrolplane.com/install.sh | bash
dsh --profile <your-profile>
```

Plugin source: [github.com/agentic-control-plane/dsh-acp-plugin](https://github.com/agentic-control-plane/dsh-acp-plugin). What ACP can see and control in dsh — the living reference — is at [/controls/dsh](/controls/dsh). Already running ACP for Claude Code, Codex, Cursor, or Hermes? The same `~/.acp/credentials` works — one workspace, every harness.

*(Installed the plugin under its launch name `dsh-plugin-acp`? Same code — it's been renamed on npm to `@agenticcontrolplane/dsh`; swap the name in your profile when convenient.)*
