Skip to content
Agentic Control Plane

DeepSeek Harness (dsh) Hooks and Permissions Reference

David Crowe David Crowe · · Updated · 9 min read
dsh deepseek-harness hooks permissions reference
Share X HN LinkedIn

Just want the answer? dsh has no shell-hook config to write. It's a native Cordis plugin registered on the harness's own typed extension points — install it into a profile and every tool call is checked from the next boot:

dsh plugin --profile <your-profile> add @agenticcontrolplane/dsh
export ACP_BEARER_TOKEN=gsk_...   # or keep it in ~/.acp/credentials
dsh --profile <your-profile>

Full dsh install guide →  ·  the controls reference →  ·  free up to 5 agents

Most coding-agent control surfaces are shell hooks bolted on after the fact: a subprocess spawned over JSON-on-stdin, matched against tool names the harness’s authors didn’t design around. DeepSeek Harness took a different path. dsh is built on Cordis, a plugin framework where the harness’s own built-in tools, its policy surface, and anything you add are all plugins on the same typed dispatch pipeline. This is the reference for that pipeline: what each stage carries, what the ACP plugin does with it, how ask/deny/allow maps into dsh’s own approval flow, and the one thing the plugin doesn’t do yet — meter model spend.

The typed interception points, not a shell hook

Every tool call in dsh — bash, pwsh, read/write/edit, glob/grep, web_fetch/web_search, subagent spawns, and the serialized sub-calls inside Code Mode — dispatches through one Cordis waterfall: tool/calltools/pre-executetools/executetools/post-executetool/result, with each listener calling next() to delegate to the next one in the chain. MCP tools ride the same pipeline, exposed as mcp__<server>__<name>.

The two extension points that matter for control:

Extension point Fires Can
tools/pre-execute before every tool call return a typed allow / ask / deny decision; a deny skips the call with the reason recorded in dsh’s 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 built-in tools use, in-process, with no subprocess and no JSON-over-stdin dialect to parse. That in-process registration is the structural difference from Claude Code’s PreToolUse/PostToolUse or Codex’s hooks.json: there’s no process boundary between the interception point and the harness, so there’s no serialization format to keep in sync across dsh releases — and no second door for a tool call to sneak through unmatched.

What each stage can do

tools/pre-execute returns one of three typed decisions: { kind: 'allow' } lets the call through; { kind: 'deny', reason } skips the call and puts the reason where the model can see it; { kind: 'ask', reason } hands off to dsh’s own approval flow (ctx.approval) rather than inventing a parallel prompt mechanism. tools/post-execute runs after the call and can accept the result unchanged, block it, or replace the content outright — a server-side block can turn a tool result into corrective feedback, and a redact can strip content before the model ever reads it. That inline-rewrite capability is rarer than it sounds: most harnesses’ post-hooks are observational, so by the time you’ve spotted a leaked credential in a tool result, the model has already read it. dsh’s post-execute point can act on it first.

The ACP plugin’s config

@agenticcontrolplane/dsh registers on both points: tools/pre-execute posts to /govern/tool-use and gets back allow / ask / deny; tools/post-execute posts to /govern/tool-output for output scanning, where a server-side block becomes corrective feedback and a redact replaces the content the model reads. It’s ~190 lines of plain ESM with zero dependencies and no build step — every transitive dependency on a plugin sitting this close to every tool call is a supply-chain surface, so the plugin keeps that surface at zero.

Override the row in your profile’s cordis.patch.yml:

- id: acp
  name: "@agenticcontrolplane/dsh"
  config:
    governBase: https://govern.agenticcontrolplane.com  # or self-hosted
    agentTier: interactive   # default: interactive when an approval service is mounted, background otherwise
    timeoutMs: 4000

ACP_GOVERN_BASE, ACP_BEARER_TOKEN, ACP_AGENT_TIER, and ACP_SHADOW=off work as environment variables. Two dsh-specific things worth knowing before you install: dsh needs Node 22Promise.withResolvers and zstd streams from node:zlib fail at boot under Node 20 with errors that don’t say why — and a bare npx dsh launch can’t resolve a profile-installed plugin, because npx resolves bare package names from its own cache rather than the profile; a --patch overlay that loads the plugin by file path works everywhere, and a normal dsh install resolves fine.

Mapping allow/ask/deny into dsh’s own approval flow

An ACP allow on tools/pre-execute lets the call run with no prompt. An ACP deny skips the call outright, with the reason recorded in dsh’s own append-only session log and shown to the model. An ACP ask doesn’t invent a new prompt — it feeds dsh’s native ctx.approval service, the same flow dsh’s own logic and any other plugin use, so the human sees one consistent prompt regardless of who raised it.

The consequential case is what happens to that ask when nobody’s there to answer it. This is dsh’s own behavior, not something the ACP plugin adds: in a headless composition with no approval service mounted, dsh resolves ask to deny by design. An unattended agent cannot self-approve. It’s one of the more deliberate unattended-safety decisions we’ve seen a harness ship natively, and it means an ACP ask rule behaves correctly without any extra configuration whether the session is attended or not — attended, it prompts; unattended, it fails closed.

On the output side, the plugin’s /govern/tool-output response can also carry shadow-mode notices — a way to see what an enforcement decision would have done on a result before you turn blocking on for that rule.

–local mode isn’t wired for dsh yet

Unlike the plugins for Claude Code, Cursor, Codex, and opencode, @agenticcontrolplane/dsh has no on-device fallback. It needs a workspace credential to function at all — the same cloud-only posture as the Hermes plugin. Run the one-command installer with --local and dsh is detected and explicitly skipped rather than silently left half-configured: the installer prints “DeepSeek Harness detected — skipped in --local mode (its ACP plugin needs a workspace; local decisions aren’t wired there yet).” Without a credential the plugin doesn’t fail closed or brick the session — it logs a loud [ACP] ⚠ UNGOVERNED: no credential warning at boot and stays out of the way, the same fail-open posture the plugin uses for a gateway outage.

Known limitations

  • Model spend isn’t metered for dsh today. There’s no proxy path for dsh’s model calls, so nothing prices tokens or attributes cost to a turn. What’s recorded instead is every tool-call decision — name, input preview, allow/ask/deny, reason, latency, and session/workspace attribution — landing in the console beside dsh’s own trajectory log as two independent witnesses to the same history.
  • No policy engine ships in dsh core. Presets like dsh-permission-presets pair a sandbox mode with an approval posture; rule content is the plugin ecosystem’s job, and the ACP plugin is one of those plugins.
  • The Claude Code hooks bridge is a compatibility path, not the recommended one. dsh’s hooks-claude-code bridge runs an unmodified hooks.json and honors deny and ask, but not input rewriting. The native plugin covers both extension points and rewrite; use it over the bridge.
  • Preview means verify after every upgrade. dsh ships breaking changes during developer preview — confirm your plugin row actually mounted with dsh --profile <name> --dump-config after updating.

Frequently asked questions

What are dsh's typed interception points?

tools/pre-execute and tools/post-execute, two stages in dsh’s Cordis dispatch waterfall that every tool call passes through — built-in tools, custom plugins, and the serialized sub-calls inside Code Mode. tools/pre-execute returns a typed allow, ask, or deny decision before the call runs; tools/post-execute can inspect and replace the result content before the model reads it.

What happens to an ask when dsh runs headless?

It denies itself. In a composition with no approval service mounted, dsh resolves ask to deny by design — this is the harness’s own posture, not something a plugin has to add. An unattended agent cannot self-approve.

Does the ACP plugin for dsh support --local mode?

No. @agenticcontrolplane/dsh needs a workspace credential — it’s cloud-only, the same posture as the Hermes plugin. Run the installer with –local and dsh is skipped with a message saying so; on-device decisions aren’t wired into the dsh plugin the way they are for Claude Code, Cursor, Codex, or opencode.

Is model spend metered for dsh today?

No. There’s no proxy path for dsh’s model calls, so nothing prices tokens. What is recorded is every tool-call decision: tool name, input preview, decision, reason, latency, and session/workspace attribution, visible in the console alongside dsh’s own trajectory log.

Share X HN LinkedIn
Get the next data drop
What agents actually cost, new tool-surface captures, and the occasional incident post-mortem — sent when we publish something worth your inbox, not on a schedule. Unsubscribe anytime.
Share: Twitter LinkedIn
Related posts

← back to blog