# DeepSeek Harness (dsh) Hooks and Permissions Reference

dsh's typed Cordis interception points: what the pre/post-execute hooks do, how ask/deny/allow maps to dsh's approval flow, and why --local isn't wired yet.

<div style="margin:8px 0 28px;padding:20px 22px;border:1px solid var(--line-2);border-radius:12px;background:var(--color-accent-light,#f0effe);">
  <p style="margin:0 0 12px;font-size:15px;line-height:1.6;color:var(--acp-text);"><strong>Just want the answer?</strong> 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:</p>
  <pre data-track="DSHHooks: Hero Config Copy" style="margin:0 0 12px;background:var(--color-surface,#faf9ff);border:1px solid var(--line-2);border-radius:8px;padding:12px 14px;overflow-x:auto;"><code>dsh plugin --profile &lt;your-profile&gt; add @agenticcontrolplane/dsh
export ACP_BEARER_TOKEN=gsk_...   # or keep it in ~/.acp/credentials
dsh --profile &lt;your-profile&gt;</code></pre>
  <p style="margin:0;font-size:13px;color:var(--acp-text-dim);"><a href="/integrations/dsh" data-track="DSHHooks: Install Guide" style="font-weight:600;">Full dsh install guide →</a> &nbsp;·&nbsp; <a href="/controls/dsh" data-track="DSHHooks: Controls Page">the controls reference →</a> &nbsp;·&nbsp; free up to 5 agents</p>
</div>

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](https://github.com/deepseek-ai/deepseek-harness), 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/call` → `tools/pre-execute` → `tools/execute` → `tools/post-execute` → `tool/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`](https://github.com/agentic-control-plane/dsh-acp-plugin) 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`:

```yaml
- 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 22** — `Promise.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.

## Related links

- [/controls/dsh](/controls/dsh) — the full controls reference: sandbox modes, the approval service, the session log
- [/integrations/dsh](/integrations/dsh) — the install guide and troubleshooting
- [Claude Code hooks reference](/blog/claude-code-hooks-reference) — the shell-hook shape, for comparison
- [Codex hooks reference](/blog/codex-cli-hooks-reference) — a narrower, evolving hook surface
- [opencode hooks and permissions reference](/blog/opencode-hooks-reference) — the other in-process plugin model, with a real rule language

## Frequently asked questions



<!-- Sources: https://github.com/deepseek-ai/deepseek-harness (dsh repository); https://github.com/agentic-control-plane/dsh-acp-plugin (plugin source and README); agenticcontrolplane.com/controls/dsh and agenticcontrolplane.com/integrations/dsh (this site's own reference and install pages, cross-checked against the above) -->
