sugarxl / dsh-safety

Listed

Safety harness plugin for DeepSeek Harness (DSH): execution-time guard, trash-based safe_delete, composition snapshots & rollback, pre-restart validation. Standalone CLI, zero dependencies.

mainOther View source

Installation

npm link # or: node bin/dsh-safety.mjs ...

This command is generated from the GitHub repository address. Inspect the upstream README and source before running it; pin a release or commit when reproducibility matters.

README

Maintainer-authored documentation snapshot.

View on GitHub ↗
Commit 341114cSynced Aug 18, 2026

dsh-safety

English | 中文

license   dependencies   node   npm version   npm downloads   github release   stars   last commit   ci

DeepSeek Harness 文件系统安全护栏:拦截破坏性操作 · 删除可恢复 · 组合可回滚 · 启动前校验
execution-time guard · safe_delete · composition snapshots · pre-restart check · standalone CLI

What

A filesystem safety harness for DeepSeek Harness (DSH). It enforces a three-tier file policy at the tool-execution boundary: destructive agent calls are denied before they run, every delete is routed through a recoverable trash, the plugin composition can be snapshotted and rolled back, and the composition is validated before a restart.

The package has zero runtime dependencies. It installs as a standard DSH profile bundle and also ships a standalone CLI, so the recovery layer remains usable when DSH itself will not start.

Background — the guard rules are derived from a real production incident: a script silently resolved the wrong path (PowerShell's $HOME is read-only) and Remove-Item -Recurse -Force deleted an entire engine runtime root. The directory was recoverable only because it was generated content; hand-authored files would have been lost permanently. The plugin turns the lessons of that incident into enforced mechanisms rather than documentation.

Features

  • Execution-time guard (ctx.tools.guard): denies destructive tool calls before they run.
    • Recursive directory deletes are blocked everywhere (rm -r/-rf, Remove-Item -Recurse, rd /s, rmdir, shutil.rmtree, fs.rm recursive, require('fs').rmSync…) — no matter which path, routed to safe_delete.
    • write/edit/str_replace_editor on protected paths (profile package.json, cordis.patch.yml, cordis.yml, lockfiles, node_modules, the deployment install dir, home patch/settings) are denied.
    • Deletes on confirm zones (the whole OS home dir, plugin sources, agent presets) are denied and routed to safe_delete.
    • run_code bodies are scanned too — arbitrary code execution cannot hide an fs.rmSync/shutil.rmtree on a protected zone behind a tool call boundary.
    • Variable-reference deletes are caughtRemove-Item "$env:USERPROFILE\.dsh\…" whose literal path only exists after expansion is denied (the reference + tail fragment is matched against protected markers).
  • safe_delete — the only sanctioned delete channel. Moves to a trash directory (recoverable via safety_undo), preview:true shows what would be removed first, refuses filesystem roots and its own state dir, and journals every delete.
  • Composition snapshotssafety_snapshot saves the whole plugin composition (per-profile manifests, patches, lockfiles, plugin package.json + cordis.patch.yml, agent presets) with SHA-256 hashes; safety_restore rolls back to a last-known-good state (current files are backed up first). Credential-bearing files are excluded by default.
  • Pre-restart checksafety_check validates UTF-8, detects mojibake (wrong-encoding round-trips, the classic "DSH won't open" cause), JSON parse errors, and duplicate plugin row ids across patch layers (the "one row, one layer" rule).
  • Audit journal + web panel — every block/delete/snapshot/restore is journaled; a "Safety Center" settings section shows trash, snapshots, journal, and one-click restore/rollback.
  • Standalone CLIdsh-safety works without DSH: delete/undo/snapshot/ restore/check from your own terminal, even when DSH won't boot.

Install

System requirements: a working DeepSeek Harness (dsh web boots). npm install has no extra requirements; installing from the repository needs Node.js >= 22 and pnpm.

dsh plugin --profile web add @suagr_xl/dsh-safety   # install from the official npm registry / 从官方 npm registry 安装

dsh plugin runs pnpm and reconciles dsh.profile.bundles automatically because this package declares dsh.bundle. Restart dsh web — the guard is then active and the safety_* tools appear.

From the repository (development)

git clone https://github.com/sugarxl/dsh-safety.git   # clone the repo / 克隆仓库
cd dsh-safety                                         # enter the directory / 进入目录
dsh plugin --profile web add link:$(pwd)              # symlink the repo into the profile / 把仓库软链进 profile

The link: protocol symlinks the repo (changes to lib/ apply after a restart), unlike file: which copies a snapshot. dsh plugin reconciles the bundle automatically. Note: the profile directory is not a pnpm workspace, so any workspace:* deps would fall back to the npm registry — this plugin has zero runtime dependencies at all (its imports are only Node builtins + its own safety-core.mjs), so a bare link: install works with no node_modules of its own and no fallback is needed.

Where it lands (official layout)

Both installs go through the official dsh plugin mechanism — nothing else to configure:

$DSH_HOME/profiles/<name>/package.json                # + dependency + dsh.profile.bundles / 新增依赖 + dsh.profile.bundles
$DSH_HOME/profiles/<name>/node_modules/dsh-safety/    # the installed package / 安装的包本体

The bundle layer is read at boot from the package's own cordis.patch.yml. The dsh-safety row id appears in exactly one layer (that file); never add it to the profile or home cordis.patch.yml.

Verify & uninstall

dsh --profile web --dump-config | grep -i dsh-safety   # row present / 确认行出现
dsh-safety check                                        # pre-restart gate / 重启前体检
# restart dsh web / 重启 dsh web

# uninstall: / 卸载:
dsh plugin --profile web remove @suagr_xl/dsh-safety
# restart dsh web / 重启 dsh web

Install troubleshooting

  • Installed, restarted, but nothing changed: restart the whole dsh web process — a page refresh is not enough. Confirm the row is mounted with dsh --profile web --dump-config.
  • ERR_PNPM_IGNORED_BUILDS: pnpm blocks dependency build scripts; add the listed packages to pnpm-workspace.yaml allowBuilds and re-run.
  • pnpm release-age gate installs an old version: pnpm 11's minimumReleaseAge can silently pick an older publish within ~10 days; add minimumReleaseAgeExclude: ['@suagr_xl/dsh-safety'] to the profile's pnpm-workspace.yaml and run dsh plugin --profile web update @suagr_xl/dsh-safety.

Standalone CLI (no plugin install needed)

npm link   # or: node bin/dsh-safety.mjs ...
dsh-safety status

The CLI reads the same $DSH_HOME/.dsh-safety state the plugin uses, so you can undo/restore from your terminal even if DSH is down.

Quick start

# 1. Inspect the effective policy zones
dsh-safety policy

# 2. Snapshot before editing any composition file
dsh-safety snapshot before-edit

# 3. Delete through the safe channel (preview first, then execute)
dsh-safety delete path/to/file --preview
dsh-safety delete path/to/file

# 4. Recover a delete
dsh-safety trash
dsh-safety undo <trash-id>

# 5. Boot failure: validate, then roll back
dsh-safety check
dsh-safety status          # list snapshots
dsh-safety restore <snapshot-id> --confirm

CLI reference

dsh-safety status                  state: trash, snapshots, journal
dsh-safety delete <path> [--force] [--preview]
dsh-safety trash [--limit N]
dsh-safety undo <id>
dsh-safety snapshot [label] [--exclude a,b]
dsh-safety restore <id> --confirm
dsh-safety check                   exit 1 on failure (CI-friendly)
dsh-safety journal [n]
dsh-safety policy                  effective policy zones
dsh-safety help

--home <path> overrides the state root ($DSH_HOME or ~/.dsh by default).

Model-facing tools (when installed as a plugin)

ToolPurpose
safe_deletetrash-based delete (preview / force / undoable)
safety_trash / safety_undolist trash / restore an item
safety_snapshot / safety_restoresnapshot composition / rollback (confirm:true)
safety_checkpre-restart validation (UTF-8 / mojibake / JSON / duplicate ids)
safety_journal / safety_statusaudit log / state

Configuration

Configure via the bundle row in a patch layer (e.g. the profile's cordis.patch.yml):

- id: dsh-safety
  config:
    blockWriteRoots: ["C:\\extra\\protected"]
    confirmDeleteRoots: ["D:\\data"]
    snapshotExclude: ["settings.yaml", ".credentials.yaml"]
    blockWrites: true
    blockShellDestructive: true
    audit: true
    keepTrash: 200
    keepSnapshots: 10
FieldDefaultMeaning
blockWriteRootsprofile manifests/patches/lockfiles/node_modules, install dir, home patch/settingsno write/edit/delete
confirmDeleteRoots$HOME, profiles/*, .agent-presetsno delete without force (still trash-only)
snapshotExclude["settings.yaml", ".credentials.yaml"]files never copied into snapshots
blockWritestrueenable the write/edit guard
blockShellDestructivetrueenable the shell-delete guard
audittruejournal destructive tool calls
keepTrash / keepSnapshots200 / 10retention limits

How it works

Three-tier policy:

TierAllowedDeniedDefault coverage
protectedreadwrite / edit / deleteprofile package.json/cordis.patch.yml/cordis.yml/lockfiles/node_modules, install dir, home patch & settings
confirmread, editdelete (needs safe_delete --force, still trash-only)entire $HOME, plugin sources, agent presets
freeread/write/deleterecursive deleteregular workspace files

The guard decision chain, per tool call: destructive verb? → is it a recursive delete? → does an explicit path hit a protected/confirm zone? → does a variable-reference fragment ($env:X\…, %X%\…, ${X}/…) expand into a protected zone? → does the command text hit a protected marker (~/relative forms)? → run_code code bodies go through the same chain → recursive deletes are denied everywhere as a final rule. Denials are journaled and returned to the model as errors (never a crash).

A second layer hooks the fs/write-intent / fs/edit-intent waterfalls and throws FS_DENIED on protected paths regardless of which tool writes.

buildPolicy lives in safety-core.mjs and is shared by the plugin guard and the standalone CLI, so the two surfaces can never drift apart. restoreSnapshot is transactional: it backs up live files first, then copies snapshot files back, and rolls the whole thing back if either phase fails — a failed rollback never leaves the composition half-restored.

Structure

dsh-safety/
├── bin/
│   └── dsh-safety.mjs        # standalone CLI (zero deps)
├── lib/
│   ├── safety-core.mjs       # pure logic: policy/guard/trash/snapshot/check
│   ├── index.js              # host half: tools, guard, fs hooks, web route
│   └── client.js             # browser half: "Safety Center" settings panel
├── test/
│   ├── safety.test.mjs       # 20 unit tests (zero deps)
│   └── harness.mjs           # 38 integration checks (clean checkout, zero deps)
├── cordis.patch.yml          # bundle patch (inserts the dsh-safety row)
├── package.json              # dsh.bundle + dsh.client + bin
├── install.ps1 / recover.ps1 # local convenience scripts (snapshot→install→verify→rollback)
├── README.md / README.zh.md  # docs (bilingual, officially paired)
└── LICENSE / NOTICE / SECURITY.md

Testing

node --test test/safety.test.mjs   # 20 unit tests, zero dependencies
node test/harness.mjs              # 38 integration checks, clean checkout (no @deepseek-ai needed)
npm run check                      # syntax checks

Troubleshooting

  • DSH won't boot after a plugin change: run dsh-safety check to find mojibake / JSON / duplicate-id problems; dsh --profile web --dump-default-config to see the bundle layer without the user layer; dsh-safety restore <id> --confirm to roll back a snapshot.
  • The guard blocks something legitimate: the guard never blocks reads or edits of plugin sources; it blocks deletes on $HOME/plugin/config zones — use safe_delete (undoable) instead of raw rm.
  • A protected path needs to be deleted: safe_delete with force:true (or dsh-safety delete --force) — the item still goes to trash, never permanent.

Security

See SECURITY.md. In short: the guard intercepts model tool calls, not commands you run in your own terminal; safety_check is a line-level scanner, not a full YAML parser. It is a safety net, not a sandbox — configure DSH's own sandbox/approval for real containment, and use this plugin for the recovery layer DSH lacks.

License

MIT. Integration patterns modeled after DeepSeek Harness (MIT); see NOTICE.

Project files and signals

Shown items are public repository signals detected in the directory snapshot.

TestsDetected
Security policyDetected
Contributing guideDetected
DocumentationDetected

Repository information

Language
JavaScript
License
MIT
Latest release
v0.1.1
Last updated
Aug 18, 2026, 11:08 AM

Install deliberately

Review source code, permissions, lifecycle hooks, dependencies and network access. Test untrusted plugins in an isolated environment.