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.

VerdictEffectWhat the agent and the human see
allowThe tools/pre-execute waterfall continues (next())Nothing added — the command runs exactly as it otherwise would
askReturns { kind: 'ask' }, which routes through ctx.approvalThe human permission prompt appears, carrying the reviewer's reason
denyReturns { 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:

  1. The call is ignored unless its tool is in tools and the calling session's current permission preset is in presets — every other session behaves as if the plugin were not installed.
  2. 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).
  3. The answer must be one JSON object — {"decision":"allow"|"ask"|"deny","reason":"…"} — and the decision is mapped onto the harness's own PreToolDecision vocabulary.

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?

PresetSandboxWhen it is the right one
autoworkspace-writeThe host has a working sandbox backend (bubblewrap / Landlock). The sandbox is the outer boundary and the review is a second gate.
auto-full-accessdanger-full-accessNo 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

KeyDefaultMeaning
presets['auto']Permission presets whose sessions are reviewed; every other preset is untouched
tools['bash','pwsh']Tool names whose calls are reviewed
provider / modelthe session's own routeRoute for the review call; supply both or neither
maxTokens512Output budget for one review answer
timeoutMs30000Wall-clock budget for one review call
onUnavailableaskOutcome when the review cannot complete (ask / deny / allow)
maxCommandChars4000The command text is truncated to this length before framing
skipPatterns[]Commands matching any of these regular expressions skip the review round trip
reasonLanguageEnglishThe 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; skipPatterns is the escape hatch.
  • Verdicts on borderline commands are not deterministic. The same command can come back allow on one call and ask on the next. Tighten the rubric in reviewSystem() 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