PluginWorld
Ds

dsh-edit-approval

dsh✓ SPEC VERIFIED

DeepSeek Harness plugin: per-edit approval with a red/green line diff before write/edit/str_replace_editor — approve once or reject (Claude Code behavior).

@SiriLee · v0.2.3 · MIT · updated yesterday

SECURITY

A

SCORE

78

INSTALLS

2K

PLUG IN

npx @deepseek-ai/dsh web

Launch, then search "dsh-edit-approval" in the built-in market to plug it in

README

dsh-edit-approval

Per-edit approval for DeepSeek Harness: every write / edit / str_replace_editor call asks before the file is touched — a red/green line-level diff, then approve once or reject; master switch in Settings → General.

npm version npm license

English | 中文

✨ Features

Feature Description
Pre-write approval Intercepts write / edit / str_replace_editor on the tools/pre-execute seam and asks before any file is modified
Red/green line diff Line-level diff (added / removed / context) computed per tool semantics; the approval panel shows only the changed lines — a right-aligned line-number gutter with `
Panel collapse Long diffs can be collapsed via the button at the strip's right end to reveal the agent's output; CSS-only hide/show, expanding restores the exact view
Approve once / reject Two actions, mirroring the Claude Code edit-approval flow; rejection reports back to the model
Master switch Settings → General "Edit approval" row, backed by the /approval-edit on|off|status host command (same source)
Policy-aware Respects the session approval policy: ask intercepts, never (full access) runs edits through untouched
Thresholds minDiffLines, includeCreate, includeDelete for fine-grained control

📸 Screenshots

Master switch in Settings → General
Master switch in Settings → General
The /approval-edit command and its arguments
The /approval-edit command and its arguments
Approval panel with the red/green line diff
Approval panel — red/green line diff

📦 Install

Published to npm — the registry path is the recommended one. Restart dsh web (--profile web) after installing.

dsh plugin --profile web add dsh-edit-approval

For contributors: local checkout (dsh plugin --profile web add /path/to/dsh-edit-approval), a pinned GitHub commit (dsh plugin --profile web add github:SiriLee/dsh-edit-approval#<sha>), or an offline tarball (npm pack then dsh plugin --profile web add ./dsh-edit-approval-<version>.tgz). A git install fails on first run until you add an allowBuilds key to the profile's pnpm-workspace.yaml (pnpm blocks git dependencies from running build scripts); after that it runs the plugin's prepare and installs it. npm pack also runs prepare, so a tarball always carries a prebuilt lib/ (with .d.ts) and the LICENSE.

How it works

The plugin listens on the tools/pre-execute waterfall (the seam the harness runs before a tool executes) and matches a whitelist of registered tool names: write, edit, str_replace_editor. For each intercepted call it:

  1. Resolves the target through ctx.fs, applying the same session-cwd rule the fs tools use (a relative .. path canonicalizes the cwd).
  2. Reads the current content and reconstructs the proposed content from the tool's arguments, mirroring each tool's semantics:
    • write — full text; edit — single unique replace (or replace_all);
    • str_replace_editorstr_replace unique replace, insert line insertion, create uses file_text.
  3. Computes a line-level diff between current and proposed content with jsdiff's structuredPatch (Myers), the same reference implementation and the same 3-line context window the harness's write/edit result cards use — so the approval preview and the post-approval result card derive from the same algorithm, and a one-line edit in a large file stays a one-line diff.
  4. Returns { kind: 'ask', reason } with a header line (tool · file (op): N insertions, M deletions) plus the diff text — change rows with a right-aligned line-number gutter and | ( 15| -xi / 15| +XI-EDITED), plus context rows and hunk gaps carried on the same structuredPatch source as the harness result cards. The harness's own serviceAsk routes that through ctx.approval into the web approval panel — the host needs zero UI changes. allowed-once proceeds, rejected denies the call; every other case delegates via next().

The browser half (dsh.client) rebuilds the panel's plain-text headline into only the changed rows — removals red, additions green, with the right-aligned NN| gutter — and a ellipsis marks skipped context runs and hunk gaps; it adds a white-space: pre-wrap compensation for the headline's CSS, installs the collapse button on multi-line diffs, and registers the Settings → General master-switch row. All side effects live in a single ctx.effect (torn down on plugin unload / HMR), and a per-animation-frame MutationObserver enhances approval panels as they appear.

Approval policy interaction

The harness's session approval policy (ask / never) keeps applying:

Session policy Plugin behavior
ask (e.g. workspace-write preset) Intercepts and shows the approval panel
never (e.g. danger-full-access preset) Delegates — edits run without prompting, the sandbox keeps enforcing

Under never, every ask this plugin emitted would be deterministically rejected by the approval service, silently breaking every edit in a full-access session. The plugin therefore stops asking and lets the sandbox enforce. It never expands access or changes the sandbox mode.

Configure

Runtime configuration lives in the edit-approval settings namespace, layered as schema defaults < cordis row config < user settings page (persisted). The cordis row ships without config on purpose; a profile patch overrides deployment defaults by restating only the keys it changes:

# profile's cordis.patch.yml
- id: dsh-edit-approval
  name: dsh-edit-approval
  config:
    minDiffLines: 2
    includeCreate: false
Key Default Description
enabled true Master switch (users can turn it off)
tools ['write','edit','str_replace_editor'] Whitelist of intercepted registered tool names
minDiffLines 0 Ask only when the change touches at least this many lines; smaller changes pass silently
includeCreate true Whether creating a new file asks for approval
includeDelete true Whether clearing/emptying a file asks for approval

Behavior details & limitations

  • Only write-family tools are intercepted; edits inside bash/pwsh commands are out of scope.
  • The diff is presented as + / - line markers — a read-only preview, not interactive per-line selection; "apply partially" is not supported.
  • Cases the tool itself would fail on are not asked about and pass through for the tool to report: str_replace_editor create against an existing file, a non-unique or missing old_str / old_string. An empty old_string edit preview deviates from the tool (treated as not-found) — the deviation is safe, it never falsely blocks.
  • The button text follows navigator.language, not ctx.locale — a deliberate simplification for a self-contained bundle.
  • Note the registered tool name is str_replace_editor (underscores), distinct from the npm package name @deepseek-ai/dsh-tool-str-replace-editor.

Not included

  • Post-edit review / rollback — covered by the community dsh-change-review.
  • Keyboard shortcuts (Enter to approve / Esc to reject) — split into a dedicated plugin.
  • Permission-tier extensions — covered by the community dsh-auto-approval-plugin.

Compatibility

  • Node.js ^22.19.0 || >=24.0.0.
  • DeepSeek Harness web profile (dsh --profile web); peer @deepseek-ai/* packages are resolved by the harness at runtime.

[!WARNING] This project and DSH are both in developer preview. Pin exact versions in reproducible environments and review the behavior notes above.

Development

npm install            # devDeps from the npm registry
npm run typecheck      # tsc on both compilation surfaces (host + client)
npm test               # vitest: diff / guard unit tests + real-cordis integration + jsdom panel-collapse (54 cases)
npm run build          # full build: tsc → lib/ (with .d.ts) + lib/client.js bundle
npm run build:portable # optional lightweight esbuild build, no typecheck
node scripts/verify-host.mjs   # verify the BUILT host artifact end-to-end

prepare runs the full build, so git installs and npm pack/npm publish always produce a complete lib/ (with .d.ts) and the LICENSE.

Publishing

Releases go out through GitHub Actions Trusted Publishing (OIDC, no stored NPM_TOKEN). See docs/npm-trusted-publishing-guide.md.

npm version patch && git push origin main --tags   # triggers .github/workflows/publish.yml

The workflow verifies the tag matches package.json, runs typecheck + tests + a full build + artifact verification, publishes with Sigstore provenance, and creates a GitHub Release. CI (.github/workflows/ci.yml) runs the same checks on every push / PR. The publish step is idempotent — a version already on npm is skipped.

Directory layout

src/index.ts            host plugin: tools/pre-execute interception + /approval-edit command + settings
src/diff.ts             line-level diff (pure functions: jsdiff structuredPatch mapping, render, counts)
src/guard.ts            decision logic (pure functions: tool matching, thresholds, create/delete, ask/pass)
src/client/index.ts     client plugin: red/green diff rendering + master switch + lifecycle
src/client/settings-row.tsx   Settings → General toggle row
tests/                  vitest suites (diff / guard / integration / client collapse)
scripts/                build + artifact verification
cordis.patch.yml        bundle patch (mounts the host plugin row)
package.json            dsh.bundle + dsh.client manifests, peerDependencies

Security

This plugin reads the target file only to compute the preview diff (at the tools/pre-execute interception point); it never writes files itself — the tool body performs the write only after you approve. It makes no network requests and accesses no credentials.

License

MIT

SIMILAR PLUGINS