dsh-risk-gate: Patching Armor Break with a human gate

Preface

When running agents with DSH, many people install patches like dsh-purge that change approval to auto-allow and default sandbox settings to open. The price is that high-risk actions like git push, recursive delete, and registry edits are no longer blocked. dsh-risk-gate does not undo the armor-breaking behavior, but adds a mechanical gate to these actions: if a rule is hit, it directly deny, and must be allowed by the user replying with a one-time code. It is maintained by oobitoooo, categorized under admin-security, has 1 star on GitHub, is a community utility, and is not an official component.

It solves what problem

Patch #9 of dsh-purge replaces @deepseek-ai/dsh-user-approval with “always allowed-once” on every request:

// Actual behavior after the host is patched
if (this.effectivePolicy(session) === "never") return "allowed-once";
return "allowed-once"; // No more popups, no more fail-closed

This means any solution relying on kind: 'ask' is equivalent to automatic consent. Changing the approval configuration back to ask is useless because the code has been rewritten; making purge apply fewer patches doesn’t work either, as it doesn’t support disabling via patch, and upgrading re-applies them.

This plugin doesn’t go through approval, but hangs its own door:

  1. Mount tools/pre-execute (a reorderable allow/deny gate), deny on match;
  2. Then mount ctx.tools.guard() for a monotonic fallback, any subsequent listener trying to flip a deny to allow will be blocked again;
  3. The credential to allow is a one-time code appearing in the user’s message, read from agent/pre-step; codes generated by the model itself are invalid;
  4. The code is one-time, retries of the same command reuse the same code, and it expires in 10 minutes.

It doesn’t do

  • Does not modify host files, inject prompt personality, or touch app.asar;
  • Does not undo other armor-breaking behaviors, such as continuous execution, tool limits, or sub-agent depth;
  • Does not connect to the internet, aside from your own policy.json, there is no outbound traffic;
  • Is not a sandbox, not a permission system. It blocks tool call parameters, not intent.

Installation

Requires DSH Desktop or dsh web, Node ≥ 18.

Method 1: GitHub Direct Install (Recommended)

dsh plugin --profile desktop add "https://github.com/oobitoooo/dsh-risk-gate/releases/latest/download/dsh-risk-gate-1.1.0.tgz"
# for web profile, replace desktop with web

To pin the version, replace latest/download/ with the specific version number download path.

Method 2: Local clone / link (for development)

powershell -ExecutionPolicy Bypass -File scripts\install.ps1            # default desktop profile
powershell -ExecutionPolicy Bypass -File scripts\install.ps1 -Profile web

Method 3: Manual (when not trusting scripts)

  1. Put the repo in $DSH_HOME\plugins\dsh-risk-gate;
  2. Add dependency "dsh-risk-gate": "file:../../plugins/dsh-risk-gate" to the profile’s package.json, and append "dsh-risk-gate" to dsh.profile.bundles;
  3. Run pnpm install in the profile directory, or manually create a junction node_modules\dsh-risk-gate pointing back to the source;
  4. Restart the client.

After installation, completely exit DSH (including tray icon) before reopening. Modifying package.json or the profile’s cordis.patch.yml will trigger hot reload. The installation command in the README points to the tgz path for version 1.1.0, while the repo’s package.json is currently version 1.3.1; use the actual Release filename when using it.

Typical Usage: Confirmation Flow

A single call that hits a rule will be intercepted, and the host returns content similar to the following:

Error: [risk-gate] 已拦截:递归删除 / 格式化文件(R1)
目标:Remove-Item -Recurse -Force "…\dist"

这是高危操作,需要用户本人确认。请把下面这段原样发给用户,然后停下等回复:
「…确认请回复:确认 8CFC6197」

After the user replies 确认 8CFC6197 (Confirm 8CFC6197), the same call will be retried once and allowed; executing the same command again will be blocked. The code is only recognized in user messages; the model writing “Confirm xxx” does not count, and confirmation in sub-agents or other sessions is not universal.

Batch Tasks: Authorization Window

In continuous actions like deployment, migration, or bulk renaming, confirming one by one is a downgrade. The plugin provides an authorization window: after the user sends “授权 30 分钟” (Authorize 30 minutes), high-risk operations are allowed directly for 30 minutes, each written to audit.jsonl (decision=allow-by-grant). Sending “取消授权” (Cancel Authorization) closes it immediately. The limit is 120 minutes, exceeding it is counted as 120 minutes. Within the window, the rule judgment does not change, it just skips the “need code” step.

Default Blocking

Rule Content
R1 Recursive delete / Formatting: Remove-Item -Recurse, rm -rf, rmtree, diskpart, Format-Volume
R2 git push, reset --hard, clean -f, changing remote, deleting release/repo
R3 Registry, services, scheduled tasks, firewall, execution policy, Defender exclusions, hosts, takeown/icacls
R4 Install / Uninstall global packages and plugins: npm/pnpm -g, winget, choco, pip install, dsh plugin add
R5 Outbound connections / Exfiltration: ssh/scp/nc/chisel/frp/ngrok, POST with body
R6 Download and execute / Encoding obfuscation: \| iex, DownloadString, -EncodedCommand, FromBase64String
R7 Force kill process / Elevation: Stop-Process -Force, taskkill /f, runas, psexec, sudo
R8 Spawning subprocess in run_code: child_process, subprocess, os.system
R9 Deleting or writing to critical paths: C:\Windows, Program Files, ProgramData, resources\app, .ssh, .credentials

R1 has an exception: when the target falls within an allowed root and the number of entries does not exceed maxEntries (default 200), it is allowed directly; those joined with ; | && do not enjoy the exception.

Configuration

Runtime reads $DSH_HOME/risk-gate/policy.json (on Windows it is %USERPROFILE%\.dsh\risk-gate\policy.json), changes are hot-read, no restart needed. The first start will automatically write the default values.

{
  "ttlMs": 600000,
  "rules": [
    { "id": "R1", "label": "递归删除 / 格式化文件", "flags": "i",
      "pattern": "remove-item[^\\n]*-recurse|\\brm\\s+-rf\\b|shutil\\.rmtree" }
  ],
  "protectedRoots": ["c:\\\\windows", "\\\\resources\\\\app", "\\\\.ssh"],
  "recursiveDelete": {
    "allowRoots": ["C:\\\\work\\\\my-project"],
    "maxEntries": 200,
    "useWorkdirAsRoot": false
  },
  "allow": ["^\\s*git\\s+(status|diff|log|show|branch)\\b"]
}
Field Effect
rules[] Intercept if any one is hit (regex, flags defaults to i)
protectedRoots[] write/edit commands hitting these path fragments are considered high-risk; delete commands also use this for R9
recursiveDelete.allowRoots Exception roots for R1
recursiveDelete.maxEntries Entry limit, exceeding it requires re-confirmation
recursiveDelete.useWorkdirAsRoot Treat workdir as an allowed root as well (default off)
allow[] Whitelist for read commands, only effective when the command is not chained together
ttlMs Validity period for the code, default 10 minutes

Other artifacts: risk-gate/audit.jsonl (rotates every 2 MB for block / allow-after-confirm / warn), risk-gate/boot.log, risk-gate/pending.json, risk-gate/last-user.json.

Audit and Risk Grading

The HMAC chain structure used for audit comes from the community project dsh-guardwall: each line has prev and h (HMAC-SHA256, key risk-gate/chain.key), checkpoint is written every 20 lines to chain.checkpoint.json, running risk_gate action=verify can detect line changes, key changes, or tail truncation. Sensitive data like ghp_, github_pat_, sk-, AKIA, Bearer …, password=… are masked before writing to disk; tokens in commands are not logged as-is. Rules can write risk: 1–10, configured with thresholds.block (default 7) and thresholds.warn (default 4).

Verification and Development

node --check lib/index.js      # Syntax check
node test/smoke.mjs            # Pure logic regression (fake ctx, no DSH startup), 33 items
node scripts/livecheck.mjs     # Run 20 sample cases with current policy.json
node scripts/pack.mjs          # Generate dsh-risk-gate-<version>.tgz

On Windows, there is also scripts\verify.ps1 (four-layer wiring health check), scripts\status.ps1, scripts\uninstall.ps1. In a new session, you can directly ask the model to “run risk_gate status”, the plugin registers a risk_gate tool (status / rules / pending / selftest / audit).

Applicable Scenarios and Notes

Suitable for DSH users already using dsh-purge/purge-community who worry about the model executing high-risk commands on its own, and for developers who want to add a human confirmation step to team agents. The plugin runs with the permissions of the current DSH process; it is recommended to read the source code and MIT license once before installing to confirm that the rules and audit behavior meet your expectations. It is a gate based on rules + code, not a sandbox, and does not isolate the file system or network.

The boundaries must be clear: it blocks call parameters, not intent; python -c "print(1)" is allowed, but using python to call os.system will be blocked by R8. The rules are regex, and there are infinite ways to bypass them. It is also not a lock; anyone with local write permission can modify policy.json to delete the rules. By design, false positives are prioritized over false negatives; better to ask once.

Summary

The value of dsh-risk-gate lies in: it does not fight against armor-breaking, but adds a traceable human gate to high-risk actions; the code is only recognized in user messages, and the audit log is verifiable and masked. If you are already using dsh-purge, it is worth a try.

Directory: https://www.skillhub.cn/plugins/oobitoooo/dsh-risk-gate
GitHub: https://github.com/oobitoooo/dsh-risk-gate

羽毛球分组比赛记分
小程序二维码

欢迎使用《羽毛球分组比赛记分》微信小程序

Xiaoye