dsh-plugin-auto-review
August 14, 2026 · View on GitHub
English | 中文
An auto mode for the DeepSeek Harness (dsh): while a session sits on a review-governed permission preset, every shell command is handed to a review model before it runs, and the model either lets it through, escalates it to the human, or refuses it.
It replaces the per-command approval prompt with a judgement, instead of replacing it with blind trust.
| Verdict | Effect | What the agent and the human see |
|---|---|---|
allow | The tools/pre-execute waterfall continues (next()) | Nothing added — the command runs exactly as it otherwise would |
ask | Returns { kind: 'ask' }, which routes through ctx.approval | The human permission prompt appears, carrying the reviewer's reason |
deny | Returns { kind: 'deny', reason } | The tool result is an error naming the reason, so the acting model can adjust |
A review that cannot complete — no model route, timeout, transport error, unparsable answer — never silently allows: it resolves to onUnavailable, which defaults to ask.
Sandbox escalation (sandbox_permissions) is deliberately not auto-answered. Leaving the file sandbox stays a human decision, so those approval requests reach the human answerer untouched.
How it works
The plugin registers exactly one tools/pre-execute listener and owns no state:
- The call is ignored unless its tool is in
toolsand the calling session's current permission preset is inpresets— every other session behaves as if the plugin were not installed. - The command, its declared purpose, the working directory, the resolved sandbox mode, and the workspace root are framed as JSON and sent as one bounded auxiliary model request (
ctx.llm.stream). - The answer must be one JSON object —
{"decision":"allow"|"ask"|"deny","reason":"…"}— and the decision is mapped onto the harness's ownPreToolDecisionvocabulary.
Nothing in the harness is patched: tools/pre-execute supplies the pending call, ctx.approval supplies the human channel, and ctx.permissionPresets supplies the mode switch that the GUI already renders.
Install
dsh plugin --profile web add dsh-plugin-auto-review
Then, in that profile's cordis.patch.yml, add the review-governed presets and the plugin row:
- id: permission
config:
defaultPreset: workspace-write
presets:
read-only: { sandbox: read-only, approval: ask }
workspace-write: { sandbox: workspace-write, approval: ask }
auto:
sandbox: workspace-write
approval: ask
name: Auto (sandbox + review)
description: Workspace-write, plus a review model on every command.
auto-full-access:
sandbox: danger-full-access
approval: ask
name: Auto (full access + review)
description: No file sandbox; the review model gates every command instead.
danger-full-access: { sandbox: danger-full-access, approval: never }
- insert:
- id: auto-review
name: dsh-plugin-auto-review
config:
presets: [auto, auto-full-access]
tools: [bash, pwsh]
reasonLanguage: English
The whole config of a patched row replaces the bundle's, so restate the presets you want to keep. defaultPreset must be explicit here because auto shares its knob bundle (workspace-write + ask) with workspace-write; the presets are told apart by the durable permission/preset selection.
Switch the current session with /permission auto, or pick the default for new sessions in Settings → Permission.
Which preset should govern the review?
| Preset | Sandbox | When it is the right one |
|---|---|---|
auto | workspace-write | The host has a working sandbox backend (bubblewrap / Landlock). The sandbox is the outer boundary and the review is a second gate. |
auto-full-access | danger-full-access | No sandbox backend is usable, so confined commands cannot run at all. The review is the gate, replacing a per-command escalation prompt with a model decision. |
Configuration
| Key | Default | Meaning |
|---|---|---|
presets | ['auto'] | Permission presets whose sessions are reviewed; every other preset is untouched |
tools | ['bash','pwsh'] | Tool names whose calls are reviewed |
provider / model | the session's own route | Route for the review call; supply both or neither |
maxTokens | 512 | Output budget for one review answer |
timeoutMs | 30000 | Wall-clock budget for one review call |
onUnavailable | ask | Outcome when the review cannot complete (ask / deny / allow) |
maxCommandChars | 4000 | The command text is truncated to this length before framing |
skipPatterns | [] | Commands matching any of these regular expressions skip the review round trip |
reasonLanguage | English | The language every reason is written in — pinned, because it lands in the human prompt |
skipPatterns is empty on purpose. A loose pattern is a hole in the gate: ^ls also matches ls; rm -rf /, so anchor whole commands (^ls( -[a-zA-Z]+)?$) or leave it empty.
Tests
npm test # node test.mjs
The suite mounts the plugin against a fake context — no harness boot, no network — and covers the allow / ask / deny paths, the framed request, route and budget selection, the unreviewed-tool and other-preset pass-throughs, skipPatterns, an agentless call, an unparsable answer, a transport failure, and a stalled review bounded by timeoutMs.
Known limitations
- Every reviewed command costs one model round trip. High-frequency read-only commands pay it too;
skipPatternsis the escape hatch. - Verdicts on borderline commands are not deterministic. The same command can come back
allowon one call andaskon the next. Tighten the rubric inreviewSystem()if you need a hard rule (for example: any read outside the workspace always asks). - The reviewer sees one command, not the conversation. It cannot judge intent that is only visible in session history, so "declared purpose does not match the command" is only caught when the command text itself shows it.
- Shell tools only. File-writing tools are left to the file sandbox and the filesystem observation policy.
- Decisions are stderr records (
[auto-review] <decision>: <command> — <reason>), not a durable session event type.
License
MIT © 2026 Guangda Liu