DeepSeek Harness gets an ACP plugin on day 3 of its developer preview
Run agents? ACP lets you see, control, and price every tool call they make — free on-device install for Claude Code, Cursor, Codex, and OpenClaw, nothing leaves your machine:
curl -sf https://agenticcontrolplane.com/install.sh | bash -s -- --local
Getting started → · or use the free cloud console → · free for individuals
DeepSeek Harness — the agentic coding harness DeepSeek released into developer preview this week — now has ACP support via dsh-plugin-acp. Two commands (npm: dsh-plugin-acp):
dsh plugin --profile <your-profile> add dsh-plugin-acp
export ACP_BEARER_TOKEN=gsk_... # or keep it in ~/.acp/credentials
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.)
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?
| 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 |
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, 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, 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 — 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.
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
fnm install 22 && fnm use 22
dsh plugin --profile <your-profile> add dsh-plugin-acp
export ACP_BEARER_TOKEN=gsk_...
dsh --profile <your-profile>
Plugin source: github.com/agentic-control-plane/dsh-acp-plugin. Already running ACP for Claude Code, Codex, Cursor, or Hermes? The same ~/.acp/credentials works — one workspace, every harness.