Maintained fork of dsh-approval-gate. A neutral operation whose confirmation count has reached the threshold auto-approves when the judge is unavailable, instead of prompting a human again; approver-facing explanations are generated in Chinese from the real sandbox mode, command and paths. Also carries deterministic hard-deny for credential exfiltration and system-path destruction, judge-input redaction, a judge model candidate chain, fingerprint-scoped allow rules, and an approval view with unified diff and one-click revert.
Install
# from GitHub (first run asks for allowBuilds approval — follow the hint, retry)
dsh plugin --profile web add github:IamNewHands/dsh-approval-gate
Any plugin you install runs third-party code with your own permissions — it can read your files, use your credentials, and reach the network, and tool approvals don’t sandbox it. GitHub-sourced plugins also run build scripts at install time — pnpm blocks those until you allow them, so an install can stop with ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED or ERR_PNPM_IGNORED_BUILDS; dsh prints the exact key to add under allowBuilds in your profile’s pnpm-workspace.yaml, and the install works on the next run. Allowing a build is a trust decision: only install sources you trust, and pin a commit (github:owner/repo#sha).
README
简体中文 | English
dsh-approval-gate
Auto-approval gate for DeepSeek Harness — minimal human intervention: safe operations auto-approve, risky ones go to a human (fail-safe).
A deterministic hard-deny layer and a Flash model pre-judge every sandbox escalation: credential exfiltration and destruction aimed at system paths are rejected outright, routine operations auto-approve, hard-risk operations (deletion / credentials / remote / system / bulk) always require human confirmation; learned rules only ever cover operations you confirmed, with an in-app human review UI.
Origin: this project is a fork of moon09300731/dsh-approval-gate, with several defects fixed and multi-machine rule sharing added. The original author's copyright and the MIT license are in LICENSE. This repository (IamNewHands/dsh-approval-gate) is the maintained version.
Upstream attribution: the deterministic hard-deny layer, the classifier-input redaction, the structured JSON verdict protocol, the judge-failure fallback, and the dynamic system-prompt guidance were ported from NanmiCoder/dsh-auto-mode (MIT License), adapted here to this plugin's confirmation-based learning pipeline. Credit for those five capabilities belongs to that project.
✨ Features
- 💾 Full export / import (v0.9.13): the "Backup & migration" card in Settings exports this machine's rules and judge settings as a single JSON file (allowlist / permanent-human rules / deny keywords / hard categories / thresholds and timeouts / scope and outside switches / judge model / learning progress) that another machine imports and starts using right away. Import is either merge (union, local rules survive, idempotent) or replace (the bundle wins); "judge parameters and judge model" can be turned off on its own, and "promote session rules to global" fixes session rules that would otherwise never fire across machines. It always backs up first (
import-backup-*.json, fully rollback-able) and a rejected import writes nothing. The bundle is allow-listed fields only, carries no machine paths, and never touches the profile'scordis.patch.yml(the permission preset still comes from "one-click configure") - 🚦 Deterministic hard-deny layer (evaluated before keywords): credential exfiltration and destruction targeting a filesystem root, an OS/credential-critical path, a Windows device namespace or a reserved device name are rejected outright with no dialog so the agent replans; the home root and the DSH-config exceptions still escalate to a human, preserving manual approval. Ordinary workspace operations are unaffected
- 🗂️ DSH config auto-approved (v0.9.5): profile / plugin / dependency changes under
$DSH_HOME(cordis.patch.yml,package.json,pnpm-workspace.yaml,skills/,node_modules/, …) no longer prompt a human — measured, 20 of the last 7 days' 36 human prompts were exactly this. Two exceptions still escalate: the gate's own data directory$DSH_HOME/auto-approve/(rewriting it means allowing itself) and credential-named files (api-key/token/secret/credential/.env/id_rsa/*.pem/*.key). This tier sits after the dangerous-keyword layer, so irreversible keywords still escalate first. (The v0.9.1 bullet "writes underDSH_HOMEalways require a human" is reversed by this release — rationale and residual risk in the CHANGELOG) - 🎯 Anchored to the command + always ask outside the workspace + sensitive paths always ask (v0.9.8): a rule's
contains/keywordsnow match real facts only (the command plus real targets), so prose no longer grants permission (measured: of 897 auto-approvals in 30 days, 30 had a command that did not contain the rule word at all —gh run viewallowed bycontains:"github", for instance). Fingerprint sources are anchored to match (re-approval / sediment / learning all take the command), and a re-approval'scontainsis now directory-level, so a new filename in the same directory needs no fresh approval. Always ask outside the workspace (outside/mixed→ human;unknownis not blocked), ordered after the allowlist, so rules you wrote for outside paths (%APPDATA%\Rime…) still apply; command-flavoured$DSH_HOMEmaintenance (pnpm --dir … install, pin updates) stays automatic through the DSH-config gap closure (54 events measured). Sensitive path shapes always ask:.ssh/.aws/id_rsa/credentials*/.env*/*.pem/*.key/.git-credentials/Cookies/Login Data/ the gate's own data directory — a.envinside the workspace is caught too, while source files likesrc/credential/transport.tsare not false positives. Measured: human prompts 147 → 178 per 30 days (+21%), buying 30 genuine over-permissions fixed plus 2 credential paths that now always ask. Both switches (scope auto-allow / always ask outside) revert in one click - 🚧 Danger fence + scope auto-allow (v0.9.7): operations inside the workspace are allowed directly by target scope (recorded as
verdict: scope, zero judge calls), while six categories still escalate to a human even inside the workspace: network fetch (curl/wget/iwr/Invoke-RestMethod/gh release download…, exempt when every URL is local), dynamic execution (iex/-EncodedCommand/FromBase64String/| bash…), package install (pnpm/npm/pip install/docker pull…, exempt when aimed at$DSH_HOME), persistence (schtasks/reg add/New-Service/Set-ExecutionPolicy…), recursive delete (Remove-Item -Recurse/rm -rf/git clean…; single-file cleanup is not fenced) and executable artefacts (writing.exe/.dll/.ps1/.bat/.git/hooks//.github/workflows/…). The fence sits before the allowlist and learning, so a sedimentedcontainsrule cannot unblock downloads — otherwise one re-approval would unblock them forever. The interception reason lands in the event and on the card (new "interception reason" row). Measured over 30 days: the fence fires 23 times (13 of them previously auto-approved), scope auto-allow saves 23 human prompts, net 10 fewer. One settings toggle switches scope auto-allow off. Honest caveat: the fence is textual matching and can be obfuscated around; it stops a model being talked into a download, not a targeted attack - 🧭 Target localisation (v0.9.6): approval records gain a "target location" row (
工作区内/工作区外/跨工作区内外/无法定域), kept separate from "blast radius" (sandbox mode). Command tools no longer lose their absolute paths (measured: 458 of 628 real command-bearing events previously yielded no absolute path at all), and relative fragments in the prose (origin/main,IamNewHands/repo,.git/objects) no longer masquerade as targets; quoted paths containing spaces survive intact and the Git-Bash form/d/...is folded ontoD:; system program locations (bash.exe) and/dev/nullare not data targets. Read-only targets go intotargetsonly, neverfiles— otherwiseGet-Content ~/.ssh/id_rsawould copy a private key into the gate's own snapshot directory...traversal and "no absolute path at all" are both recorded as无法定域(fail-closed). The judge receives these real paths too instead of guessing from wording. The deterministic verdict logic is unchanged (hard deny / dangerous keywords / allowlist / learning are untouched), but the judge's input changed, so the judge layer may rule differently on command-flavoured calls - 🔬 Dangerous keywords match on boundaries (v0.9.1): a keyword only matches at a word/flag boundary, so
git push --force-with-lease(the safe lease-guarded force push) anddocker rmiare no longer read aspush --force/docker rm; a genuinepush --forcestill escalates to a human as before - 🔐 Pickable approval scope (v0.9.4): when re-approving you choose this once / this session / global (session by default; "this once" writes no rule and only allows the retry), and the settings page's "new approval sediment scope" decides where automatic sediments are written. Learning counters are isolated per
session|tool|mode|category, so no session can farm a rule that governs other sessions. Rules you wrote, repository-seed rules, built-in defaults and rules sedimented before this upgrade are all global; a session rule can be promoted to global in one click from the allow-list card. denyRules (operations you rejected) deliberately stay global: on the rejection side crossing sessions only adds a human prompt and never allows anything - 🧱 Origin fence compares scheme and port (v0.9.3): an origin must be literally same-origin with the request's own Host (scheme + host + port);
javascript:/data:/nullorigins are rejected outright; only "the request Host is loopback and the origin is a loopback alias" may differ in port (127.0.0.1 / localhost / [::1]). Literally same-origin DNS-rebinding pages are stopped by the credential fence (see the GUIDE) - ⚡ Flash risk pre-judgment: every sandbox escalation is judged by a Flash model, which must return a strict JSON verdict
{decision, reason, category}withdecision∈allow/ask/denyandcategory∈deletion/credential/remote/system/bulk/neutral; recoverable operations auto-approve - 🎛️ Pickable judge model (v0.9.2): the settings card "Judge model · decoupled from the main model" selects the judging model from dropdowns (route + model, "follow the agent default model" first) — no hand-editing
allowlist.json; when the model catalog is unavailable it degrades to plain text inputs. A wrong judge model is the usual cause of "every approval goes to a human" (a multi-step tool-loop model cannot finish within the judgement timeout) - 🔒 Classifier-input redaction: private key blocks, cloud/GitHub/Slack tokens,
Bearerheaders andkey=valuesecrets become[redacted-secret]; text is truncated to 1000 characters; arguments are sanitized by field name (secret-named fields →[redacted-secret-field], bulk-content fields →[redacted-<key>:<length>-chars]). Secrets never leave the machine - ♻️ Judge-failure fallback (absorbed; semantics corrected in v0.8.0): an unavailable judge prompts a human on the first failure — a broken judge does not mean your operation is harmful. The real failure reason (timeout / upstream error / empty answer) is written to the audit log and the approval record, and approving the operation seeds a fingerprint-scoped allow rule so the same target never reaches the judge again.
judgeFailureLimit(default 1; set >1 for the old "silently reject first" rhythm) - 🔗 Judge model candidate chain (v0.8.0): configured judge → session default model → built-in fallback, so one flaky channel (502 / timeout) no longer means the judge is entirely unavailable
- 🧮 Learning outranks the judge (v0.8.2): a
neutraloperation whose confirmation count already metriskyThresholdauto-approves on the completed confirmations when the judge is unavailable, instead of prompting a human again because the judge is flaky. Hard denies / hard facts / dangerous keywords / the allowlist still run before the judge, and hard categories still require a human every time - 🧾 Escalation prompts always name the command and target (v0.8.3): every escalation explanation carries both a "command" and a "target path" line, writing
host未提供when the host supplied neither;danger-full-accessadds "does not restrict paths — this grant covers the whole machine" so it is not misread as a scoped grant. When the strictcallIdmatch misses, the command is backfilled from the newest same-name call and labelled as such (display only — it never feeds hard denies, rules or snapshots) - 🩺 Session-event access fixed at the root (v0.8.4): the plugin always read
session.events, but DSH'sSessionexposes onlysnapshotEvents()/ownEvents()— so structured arguments, the deterministic hard-deny layer, and the judge's user-authorization source were all dead. It now readssnapshotEvents() → ownEvents() → events. Behaviour change: calls writing intoC:\Windows\...,~/.sshand other protected locations are hard-rejected with no dialog, and the dangerous-keyword layer can see the real command, so keyword hits escalate to a human more often - 🧹 No phantom command line (v0.8.5): only command-flavoured tools (
pwsh,bash,terminal-bash, …) print a command line;write/editshow just the target path, so a file approval never carries a meaningless命令:host未提供 - 📊 Approval explanations become a field table (v0.9.0): each approval record renders five fields — operation type / target path / blast radius / command / model note. The operation type is decided at command level (
Remove-Item/rm/format/git reset --hard→ deletion,git … push/gh release/npm publish/scp→ push/publish,write→ create/write,edit→ modify), the blast radius states the consequence of the sandbox mode (whole machine / workspace / read-only), and values are colour-coded: deletion red, create green, modify blue, push amber. The host card's plain text is now one field per line (操作:… / 路径:… / 影响:… / 命令:… / 原因:…) instead of a prose paragraph, and the notice strip shows a one-line summary. Events carry a newfactsfield (older events get them derived by the events API from what was recorded, with nothing written back); events withoutfactsstill render from text - 🔢 The "审批" tab shows a count (v0.9.0): the tab reads
审批 (12)— the number of records the view actually lists for this session (pending approvals live only in the notice strip and are excluded) — so you can tell at a glance whether anything new arrived without opening the tab - 🧭 Dynamic system-prompt guidance: while the
auto-approvepreset is active, an<auto_approve_policy>block is injected into the session's dynamic runtime context, telling the agent that routine workspace work runs directly, that deletion is the highest-risk routine operation and its authority must never be generalized to a variable/glob/parent/sibling/second target, that a reversible move or backup is preferred, and that hard-denied calls are rejected without a dialog so it should replan rather than resubmit - 🛡️ Hard risks are always human: deletion, credentials, remote/production, system paths, and bulk irreversible operations go directly to human — no counting, no learning, and no reconsideration. This is a symmetric safety gate: whether the judge answers
allowordeny, a hard category goes to a human (fixed in v0.7.0 — thedenybranch used to silently reject first, making the configured hard categories inert) - 🎯 Confirmation-based learning: within one session, after N human confirmations of the same operation the N+1th occurrence auto-approves; persisted rules carry an operation fingerprint (so only operations you confirmed are auto-approved), written at the scope chosen in the settings page (this session / global)
- 🧠 Semantic similarity verification: operations with different wording but the same intent are judged by Flash against your confirmed samples — no keyword dependency
- 📄 File diff & revert (v0.5.0+): click a file in an approval record to view a unified diff — changed lines with ±5 context lines, multiple changes grouped into hunks separated by gray "N unmodified lines" bars, green additions / red deletions / gray context, dual line numbers; one-click Revert sends a command for the AI to restore the file from snapshot
- 🗂️ Session-scoped snapshots (v0.5.0+): snapshots belong to the event's session; the approval view shows only the current session's snapshot stats; clearing supports "this session only" vs "clear all" to avoid wiping other sessions' unviewed diffs
- 🔧 Hot-reloadable config:
allowlist.jsonedits take effect immediately, no restart - 🈶 Chinese approval explanations (v0.8.1): approval cards and the approval view explain themselves in Chinese. When the justification is English or carries the host prefix
escalate sandbox to …, the plugin builds the explanation from real facts — target sandbox mode, the real command, the real target paths — and states the consequence (writable scope, reversibility, whether anything outside the workspace is touched). Commands and paths are kept verbatim and the model's original text is appended as a note. A newzhfield carries the Chinese text on each event whilejustificationkeeps the original for audit - ✅ Human review UI: a green notice appears above the composer on auto-approval; the "Approval" view (right of Trajectory) shows the current session's full auto-approval timeline
- 🔔 Rejection notices persist (v0.7.0+): notices for silent and manual rejections no longer vanish after a few seconds — they stay above the composer (with "View approval log" / "Re-approve" actions) until you switch to the "Approval" tab. The read position is persisted locally, so a reload neither loses pending rejections nor re-nags about ones already seen. A reconsidered rejection is released, so it neither counts as pending nor re-surfaces as a red notice (v0.7.1)
- ↩️ Reconsider a rejection (v0.7.0+): a silent rejection in the approval log can be re-approved in one click — writing an auto-approve rule carrying the operation fingerprint and delivering a retry instruction so the AI re-runs it. Fences: the deterministic hard-deny tier and hard-risk categories are not reconsiderable (offering a button there would be a false promise)
📸 Interface Overview
① Approval View

The "Approval" tab (right of Trace) lists the current session's auto-allowed and manually-approved actions in reverse-chronological order: each record shows the tool (bash / edit), a verdict tag ("Auto-allowed · Flash safe", "Approved" etc.), timestamp and description. The top bar shows this session's diff snapshot usage (2.9 KB · 3 items) with two cleanup options: "This session only" (removes only the current session's snapshots, never touching other sessions' unviewed diffs) and "Clear all" (double-confirmed, clears every session).
Silently rejected records (red "Rejected outright") carry an extra "Re-approve" button: pressing it writes an auto-approve rule carrying the operation fingerprint and delivers a retry instruction so the AI re-runs the operation. A pending N badge next to the title shows how many rejections are still unreconsidered. Once reconsidered, the record flips to "Reconsideration approved · … (was rejected outright)" in the done colour and drops out of the pending count. The deterministic hard-deny tier (credential exfiltration / system-path destruction) and hard-risk categories (deletion / credential / remote / system / bulk) do not show the button — no allowlist rule can override the former, and the latter must be confirmed by a human every time, so offering a button would be a false promise.
② File Diff

Click a file in an approval record to open the diff dialog: a unified diff with green additions (+), red deletions (-) and gray context lines; dual old/new line numbers on the left; multiple changes grouped into hunks with gray "6 unmodified lines" separators folding unchanged regions. The header shows +2 / -2 changed · 20 unchanged. The Revert button at the bottom sends an undo command to the conversation so the AI restores the file from the pre-approval snapshot.
③ Settings · Auto-approval

The "Auto-approval" section in Settings provides full configuration: preset initialization (one-click write of the auto-approve preset into cordis.patch.yml), backup & migration (full export / import, v0.9.13), pipeline overview, deny-keyword blacklist (built-in entries + custom add), and hot-reload notes (changes take effect immediately, no restart).
The judgment pipeline runs in this order:
hard-deny (credential / system-path) → hard-fact human escalation → dangerous keywords
→ danger fence (v0.9.7) → sensitive paths always ask (v0.9.8) → DSH-config auto-approval
→ allowlist (command-anchored, v0.9.8) → scope auto-allow (v0.9.7) → always ask outside the workspace (v0.9.8)
→ deny-rules → structured JSON judge (hard categories → human, ahead of allow / deny;
neutral → confirmation-based learning; failure limit) → verdict learning
🚀 Quick Start
dsh plugin --profile web add "github:IamNewHands/dsh-approval-gate#main"
The
dsh-approval-gatepackage on npm is upstream 0.5.0 and lacks this repository's fixes — do not install it by package name.
- Add the permission preset: append the
auto-approvepreset to~/.dsh/profiles/web/cordis.patch.yml(see guide) - Restart
dsh web - Select the preset: choose "Auto Approval (Flash)" in the session's permission dropdown
📖 Docs
📄 License
MIT
Links
More in this category
kenryu42/cc-safety-net★ 1900
Pre-execution guard that blocks destructive Git and filesystem commands and access to secrets such as SSH keys and .env files before a tool call runs.
ADWMC/helm-d#helmd★ 984
Security-analysis plugin for DeepSeek Harness: 33 tools spanning six security domains, evidence/reporting and toolbox, plus the case lifecycle, routing and tool ledger; a session-bound case workflow with an evidence chain, E-id validation and reproducible findings; 362 on-demand references; three hot-swappable persona profiles; and a four-tab workbench (armor / case / intercept / about) with case-state recovery after context compaction.
toby-bridges/api-relay-audit★ 904
Runs local security audits of AI API relays and LLM proxies from DeepSeek Harness, producing Markdown reports for prompt injection, model substitution signals, tool-call rewriting, error leakage, stream integrity, and profile-gated Web3 risks.
SeaOf0/dsh-redteam-model★ 807
Authorized-security DSH collection: nine work modes (redteam coordinator, pentest, code audit, binary analysis, attack-defense, AV evasion, incident response, cloud security, CTF solving) and fifteen runtime plugins, managed from a settings page with one-click deploy, install, update and uninstall.
howmp/dsh-pentest★ 616
Authorized pentest mode for DeepSeek Harness — exploration chain, assets and findings with a Web view.
PerryLink/dsh-auto-review★ 244
Second-model auto-review on the approval answerer chain: a read-only reviewer subagent returns structured allow/deny verdicts with reasons, fail-closed by default.
Community comments
Comments are public GitHub Discussions. Loading them connects to GitHub and Giscus; a GitHub account is required to post.