P0–P4 deterministic-first permission gate with permissive tier, learning sedimentation and approval-history UI; hard-deny credentials and protected paths, auto-allow internal read-only tools.
Install
# from npm (prebuilt)
dsh plugin --profile web add dsh-perm-gate
# from GitHub (first run asks for allowBuilds approval — follow the hint, retry)
dsh plugin --profile web add github:drscrewdriver/dsh-perm-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 README
- 中文 README
- 日本語 README
- 한국어 README
- Installation guide
- 中文安装指南
- 日本語インストールガイド
- 한국어 설치 안내
- Changelog
- 日本語 changelog
- 한국어 changelog
Compatibility note: v2.0.0 ships
ja/kodictionaries, but official DSH exposes onlyzh/enthroughLocaleRuntime(LOCALE_IDS = ["zh", "en"]). On stock DSH, selectingja/kofails withlocale "<id>" is not registered. Use a DSH fork that updatesLOCALE_IDS(locale-settings.ts) andLOCALESlabels (client/index.ts), then rebuild.
▼ DSH version compatibility
Two DSH lines are served from two long-lived branches, each with its own version series,
engines.dsh, and npm dist-tag (release layout):
DSH version Branch Version npm tag 0.1.0-rc.7 ~ 0.1.1-rc.x legacy1.x@legacy0.1.2-alpha.1+ (incl. 0.1.5-rc.2) main2.x@latest/@dsh-0.1.2(@2.xis a range)The series number tracks the DSH line (
1.x= DSH ≤ 0.1.1,2.x= DSH 0.1.2+), and the majors fence each other: a^1.xinstall never resolves a2.xrelease and vice versa.engines.dshstates the same split but DSH never reads it — the ranges and dist-tags are what hold an old DSH on1.x.
@deepseek-ai/dsh-client-runtimewas removed at0.1.2-alpha.1— it did not merely move. Thelegacyline still reachesctx.slotsthrough it;maingets the same declaration from@deepseek-ai/dsh-client-ui-renderer/client. Two version-sensitive seams are handled by capability probes rather than version checks: (1) settings registration usesregister, which exists on both lines (installSectionis an addition, not a replacement); (2)effectivePolicyis a private method of the user-approval service on both, so it is read behind atypeofprobe and degrades to “policy unknown” when absent or throwing.
Version 2.0.0 — see the Changelog.
A single, self-sufficient, deterministic-first, fail-closed permission gate for DeepSeek Harness.
dsh-perm-gate decides every tool call through a fixed priority chain:
| Stage | Decision | What it is |
|---|---|---|
| P0 | deny |
deterministic hard-deny: credential material, protected-path mutation, dangerous shell |
| P1 | allow |
a precise, bounded session grant |
| P2 | deny/allow/ask |
static rule chain: blacklist first, then allow, then ask |
| P3 | allow/deny/ask |
optional LLM semantic classifier (default off) |
| P4 | ask |
official approval seam |
Strictly fail-closed: a P0 decision is never overridden by a grant, a rule, the classifier, or a human.
Why
The DSH safety ecosystem splits this across several plugins (dsh-permission-rules,
dsh-auto-mode, dsh-auto-review, dsh-movein-permissions). dsh-perm-gate merges the
gate + approval + (optional) classifier into one package, with one audit trail and no
cross-plugin version coupling.
Features
- Command whitelist / blacklist — matched on an argv decomposition (not a raw string),
with recursive descent into
sh -c/bash -c, pipeline detection, redirect-target checking, and recursive/force (rm -rf) recognition. - Deny priority — a matching deny rule wins over any allow rule.
- Session grants — precise
(tool, canonical fingerprint)grants withTTL+maxUses; re-running with a different target never reuses authority. Sub-agents inherit but cannot mint. - Pure-function rule engine — glob/regex compilation with a ReDoS bound, loud fail on malformed rules, and source-hash compile caching.
- Audit — every decision is logged as an
{ignorable:true}event with itscallId; the model-visible reason matches the recorded outcome. - 自动审查 tier (
permissive) — an independent approval mode (separate from read-only, full-access and whitelist tiers) that is neither "auto-approve" nor blanket trust. Front-end exposes a single switch (permissive); the four backend strategies are combinable and driven by plugin settings — still fail-closed against P0. The permission picker and the settings row both show it under the product label 自动审查, with no icon. - Sandbox-escalation auto-answer (
trustEscalation) — a sandbox escalation is asked from inside the shell / pwsh / edit tool body, aftertools/pre-execute, so the gate never saw it and a call it auto-allowed still prompted you to approve the widening. With this strategy on, the exact call the gate cleared (matched bycallId) is answered here instead. - Risk-graded
llmAssist— a custom OpenAI-compatible LLM grades eachaskassafe/risky:<category>; hard categories (deletion, credential, remote, system, bulk) always ask, neutral enters verdict learning, and every failure stays fail-closed. - Verdict learning — neutral-risk asks that the human approves and that actually execute count up; after the threshold, the exact same operation (fingerprint-matched) auto-allows.
- Decision event feed — every decision is appended to a JSONL feed and surfaced by the browser half as a notice strip above the conversation input plus an approval-records tab (newest first) in the conversation view.
A preset deny-keyword blacklist (inherited from dsh-approval-gate's DEFAULT_DENY_KEYWORDS:
rm -rf, push --force, drop table, mkfs, git reset --hard, docker system prune, …) vetoes
any call whose text contains a keyword — case-insensitive substring, applied before whitelist,
grants and LLM. It is editable as a list in the settings card (preset entries are tagged, and a
one-click restore brings the preset back); unset or empty applies the preset — the blacklist
never silently turns off.
Install
Requires an existing DeepSeek Harness installation.
dsh plugin --profile web add dsh-perm-gate
Full install, upgrade, migration and troubleshooting steps live in the Installation guide — also available in 中文 / 日本語 / 한국어.
Configuration
Add the plugin to cordis.yml:
- id: dsh-perm-gate
name: dsh-perm-gate
config:
rulesFile: ./permissions.yaml # optional; defaults to $DSH_HOME/perm-gate/rules.yml
dshHome: $DSH_HOME # root pinned for protected-target checks
defaultAction: ask # allow | ask | deny
gatePresets: [permissive] # tiers where the gate is active at all (default)
Rules file
permissions:
defaultAction: ask
deny:
- command: [rm#recursive]
reason: no recursive rm
- command: [ssh]
reason: no direct ssh
- paths: [.dsh/**]
reason: protect harness metadata
allow:
- command: [pnpm, node]
reason: dev tools
- command: [curl, wget]
args: ["https://*.example.com/*"]
reason: allowed endpoint
ask:
- command: [bash, sh]
reason: ask shells
A command entry word#flag matches the command word (word) with the modifier recursive or
force — so rm#recursive matches rm -rf, env rm -rf, and sh -c "rm -rf /".
The 自动审查 tier (machine value permissive)
自动审查 is an independent approval tier in the DSH permission picker, parallel to Read Only / Workspace Write / Full access / Whitelist. It is not generic "auto-approval" and never mints blanket authority: it only narrows or widens the seam before the human/LLM step while P0 hard-deny stays monotonic and non-negotiable.
The picker label is a host-supplied product string, not a per-locale dictionary entry: DSH
0.1.2 renders a plugin tier's name: verbatim on both permission surfaces (the General-settings
default row and the composer picker) and only supplies its own localized labels for the three
built-in values, so cordis.patch.yml ships the Chinese label for every session. The tier draws
no icon — the composer renders glyphs only for the built-in values.
In cordis.yml:
- id: dsh-perm-gate
name: dsh-perm-gate
config:
rulesFile: ./permissions.yaml
defaultAction: ask
permissive: true # single front-facing switch (independent tier on)
permissiveStrategies: # backend, combinable
trustAutoAllow: true # in-scope safe ops auto-allow; dangerous/unknown ask
alwaysConfirm: false # every crossing asks; allow-controls add repeat-allow / wl-migrate buttons
llmAssist: false # LLM classify first, human fallback on ask/failure
trustEscalation: true # a cleared call's own sandbox escalation needs no prompt
trustAutoAllow is the baseline middle tier (rule-allow auto-passes). alwaysConfirm surfaces
the approval panel for every crossing; its "allow controls" add two extended buttons —
repeat-allow this type this session (a bounded session grant) and allow every occurrence
(which persists the command word into the permissions.yaml allow whitelist via
approveAllowEverywhere). llmAssist consults a real, configurable LLM (classifierEndpoint /
classifierModel, any OpenAI-compatible API) to auto-decide an ask, and falls back to the
human seam on ask/error — always fail-closed. trustEscalation (on by default while the tier
is on) answers a sandbox_permissions escalation raised from inside a call the gate already
allowed; see below. When permissive is off, the gate behaves exactly as before.
Sandbox escalation: why a safe verdict still prompted
A tool call can raise two independent approvals. The gate owns the first — its own ask, on
the tools/pre-execute waterfall. The second comes from approveEscalation inside the tool
body, at tools/execute time, whenever the model passed sandbox_permissions +
justification; by then tools/pre-execute has already settled, so the gate's allow never
reaches it. A call the LLM graded safe and the gate auto-allowed therefore still showed a
prompt asking you to approve the sandbox widening.
trustEscalation closes that gap. The gate remembers every call it positively allowed (keyed by
the host's callId, which the escalation request repeats) and answers the escalation
allowed-once itself. It applies only when all hold:
- the 自动审查 tier is on and
trustEscalationis on; - the request carries a
callIdthe gate cleared, with a matching tool name; - the reason is a recognized escalation naming
workspace-writeordanger-full-access.
Everything else — an unrecognized reason, a different call, a call the gate asked or denied, the
approval: never passthrough — delegates to the human unchanged, so a future DSH wording change
fails closed rather than open. The auto-answer is recorded on the event feed
(verdict: "escalation-auto", mode: <target>). Turn the switch off to keep widening
human-gated while other allows stay automatic.
Risk-graded llmAssist, verdict learning, and the event feed
When llmAssist is on, the configured LLM grades one ask at a time with a structured
protocol. Grading happens inside the gate's tools/pre-execute waterfall, before the decision
is returned to the host: a safe verdict delegates the call straight through, so the approval
panel never appears; only a genuinely uncertain verdict reaches you. Two receiver sources are
selectable in the settings card: a custom API (any
OpenAI-compatible endpoint — classifierEndpoint / classifierModel / classifierApiKey,
with presets including Xiaomi MiMo https://api.xiaomimimo.com/v1), or the DSH host model
group (the session's configured llm service, via agentDefaultModel.currentSelection,
optionally overridden with classifierProvider / classifierModel). A health test button
(POST /api/dsh-perm-gate/health) runs one minimal completion and reports latency.
safe→ the call is auto-allowed (audited as theclassifiersource); no panel is shown.risky+ a hard category (deletion,credential,remote,system,bulk) → the call is auto-denied without a panel; hard risks are never auto-allowed and never learned.risky:neutral→ withriskLearningenabled (Settings card, off by default), each human approval that actually executes (settled via the host'stools/resultevent) counts toward atool|categorykey; once the count reachesriskThreshold(default 3) and the new call's operation fingerprint (command word + target basename) matches a confirmed sample, the exact same operation auto-allows. A different target never reuses that authority. With sedimentation (riskSediment, on by default) a threshold-reached key's confirmed samples become deterministic allow rules: an exact hit allows outright without another LLM call — even withllmAssistoff — and the sedimented rules are visible and manageable (terminate / remove) in the settings card.- Timeouts (
riskTimeoutMs, default 20 s, one retry), transport failures, and off-protocol model output leave the ask untouched — the gate never guesses.
Learning state persists to a plugin-owned JSON ($DSH_HOME/perm-gate/learning.json, or
learningFile), never into your YAML rules file. Every decision is appended to
$DSH_HOME/perm-gate/events.jsonl (or eventsFile) and served at
GET /api/dsh-perm-gate/events?sessionId=&since=; the browser half polls it and shows the
latest decision as a notice strip above the conversation input (asks stay visible until the
next event) and lists the whole session's decisions newest-first in the Approvals tab of
the conversation view.
Every decision's affected files are snapshotted before the change lands (≤5 files, ≤256 KB each)
under $DSH_HOME/perm-gate/snapshots/; in the Approvals tab each file chip opens a line diff
(GET /api/dsh-perm-gate/diff) with a revert action that delivers a restore instruction into
the conversation (POST /api/dsh-perm-gate/revert). A snapshot inventory bar clears them per
session or entirely (GET /api/dsh-perm-gate/snapshots-stats / POST /api/dsh-perm-gate/snapshots-clear).
An ask the gate routes to a human is tracked until the human answers: a passive
approval/request observer records the closed outcome (allowed-once → approved,
rejected → rejected, cancelled → cancelled, unavailable → a denial, since no approval
channel existed), with tools/result settling the same ask as a fallback when the observer cannot
correlate it. Approvals report the post-approval learning progress (n/threshold), and the notice
strip labels all three terminal states.
The tier draws no icon in the permission picker: the composer's glyphs are keyed to the three built-in values, so a plugin-contributed tier is text-only on every surface.
A selectable session tier
cordis.patch.yml adds a permissive preset (sandbox: workspace-write, approval: ask, name
自动审查) between Workspace Write and Full access. The DSH bundle patch replaces the whole
permission.config.presets map rather than merging per key, so the file also restates the three
built-ins (read-only / workspace-write / danger-full-access, from
@deepseek-ai/dsh-base/cordis.patch.yml); test/patch-presets.spec.ts pins that key set. So the
session permission picker offers 自动审查 as an independent selectable approval tier, not a
generic "auto-approval" mode.
The gate is active only in the tiers listed in gatePresets (default ['permissive'], the
tier this plugin adds). In every other tier — Read Only, Workspace Write, Full access,
custom — the gate's decision flow does not run at all: no allow, no ask, no deny, no P0
hard-deny, no deny-keyword veto, and no audit event. The selected tier's own policy governs the
call, which is the point: danger-full-access is defined as "full access without approval
prompts", so overruling it with an ask (unanswerable there — the approval seam rejects before any
answerer runs, producing the user rejected tool "..." with no panel) or with a hard-deny would
silently contradict the tier the user chose. gatePresets: ['*'] makes the gate global again
(hard-deny included); inside an active tier an ask is still degraded to passthrough when the
session's effective approval policy is never.
Configurable in the UI
The tier is also adjustable at runtime from Settings → Plugins → 自动审查
(a settings.plugins.tab page rendered by the plugin's browser client): one switch toggles
permissive, and four toggles edit the backend permissiveStrategies. The host reads the
namespace live, so a change applies to the next tool call without a restart. This is an
independent approval class, NOT a generic "auto-approval" mode.
CLI
Dry-run one call against a rules file (no harness needed):
dsh-perm-gate --rules permissions.yaml --tool bash --args '{"command":"pnpm install"}'
dsh-perm-gate --rules permissions.yaml --list
Development
npm run typecheck
npm test
npm run build
License
Links
More in this category
toby-bridges/api-relay-audit★ 832
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.
howmp/dsh-pentest★ 451
Authorized pentest mode for DeepSeek Harness — exploration chain, assets and findings with a Web view.
SeaOf0/dsh-redteam-model★ 417
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.
PerryLink/dsh-auto-review★ 164
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.
PerryLink/dsh-permission-rules★ 114
Claude Code-style declarative permission rules: ordered allow/deny/ask YAML rules matching tool names, arguments, workspace paths, and agent identity on the tools/pre-execute waterfall, with full session-log audit, dry-run mode, and hot reload.
PensiveFei/dsh-secure-audit★ 85
Read-only security and compliance plugin for DeepSeek Harness: prompt-injection detection, Chinese-PII redaction, and a local configuration audit with redacted, reproducible reports.
Community comments
Comments are public GitHub Discussions. Loading them connects to GitHub and Giscus; a GitHub account is required to post.