DeepSeek Harness plugin

dsh-safety

A safety harness plugin for DeepSeek Harness (DSH): protected-path enforcement, trash-based safe_delete with undo, last-known-good composition snapshots, pre-restart composition checks, and an audit

Jump to install

Source facts

Repository
sugarxl/dsh-safety
Latest update
Aug 19, 2026
Category
Security & Permissions
GitHub stars
1
Format
plugin
Catalog evidence
Upstream dsh.bundle evidence
Evidence path
package.json#dsh.bundle
Checked against
0.1.0-rc.8
Upstream check date
2026-08-20

This evidence comes from the upstream catalog. This site has not installed, run, or security-reviewed the plugin.

Install

Start with a prompt that asks an agent to review the GitHub repository and source. Switch to the command if you want to install it yourself.

Copy this prompt into DSH, Codex, or another agent and ask it to review the GitHub repository and source first.

Do not install or run any commands yet. Read this plugin's GitHub repository, README, and relevant source code. Then answer the questions below clearly and directly so I can decide whether it fits my needs:

1. What is this plugin, and what problem does it solve?
2. Who is it for, and what are its typical use cases?
3. How is it used after installation? Include one minimal example.
4. What known limitations or privacy, security, compatibility, or maintenance risks does it have?
5. Give a clear recommendation: recommend, conditionally recommend, or do not recommend, with reasons.

Distinguish statements documented by the repository, inferences from source code, and unknowns. If evidence is insufficient, say so explicitly. Do not guess or simply repeat the README.

GitHub: https://github.com/sugarxl/dsh-safety
Plugin: dsh-safety
Author: sugarxl

Check the source files

Read the README and other files from this plugin directory before installing.

File explorer4 files
README.mdSource · read only
README language

dsh-safety

English | 中文

<p align="center"> <a href="https://github.com/sugarxl/dsh-safety/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue?style=flat-square" alt="license"></a> &nbsp; <a href="https://github.com/sugarxl/dsh-safety/blob/main/package.json"><img src="https://img.shields.io/badge/dependencies-0-brightgreen?style=flat-square" alt="dependencies"></a> &nbsp; <a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-%3E%3D22-339933?style=flat-square" alt="node"></a> &nbsp; <a href="https://www.npmjs.com/package/@suagr_xl/dsh-safety"><img src="https://img.shields.io/npm/v/@suagr_xl/dsh-safety?style=flat-square" alt="npm version"></a> &nbsp; <a href="https://www.npmjs.com/package/@suagr_xl/dsh-safety"><img src="https://img.shields.io/npm/dm/@suagr_xl/dsh-safety?style=flat-square" alt="npm downloads"></a> &nbsp; <a href="https://github.com/sugarxl/dsh-safety/releases"><img src="https://img.shields.io/github/v/release/sugarxl/dsh-safety?style=flat-square" alt="github release"></a> &nbsp; <a href="https://github.com/sugarxl/dsh-safety/stargazers"><img src="https://img.shields.io/github/stars/sugarxl/dsh-safety?style=flat-square" alt="stars"></a> &nbsp; <a href="https://github.com/sugarxl/dsh-safety/commits/main"><img src="https://img.shields.io/github/last-commit/sugarxl/dsh-safety?style=flat-square" alt="last commit"></a> &nbsp; <a href="https://github.com/sugarxl/dsh-safety/actions"><img src="https://img.shields.io/github/actions/workflow/status/sugarxl/dsh-safety/test.yml?style=flat-square&label=CI" alt="ci"></a> </p>

<p align="center"> <strong>Filesystem safety harness for DeepSeek Harness</strong><br> <em>execution-time guard · user-gated approvals · trash-based deletes · composition snapshots · pre-restart checks · standalone CLI</em> </p>

<div align="center">

[What](#what) · [Features](#features) · [Approval workflow](#approval-workflow) · [Install](#install) · [Quick start](#quick-start) · [CLI](#cli-reference) · [Tools](#model-facing-tools) · [Config](#configuration) · [How it works](#how-it-works) · [Structure](#structure) · [Testing](#testing) · [Troubleshooting](#troubleshooting) · [Security](#security) · [Design](docs/DESIGN.md) · [FAQ](docs/FAQ.md) · [Known limitations](docs/KNOWN-LIMITATIONS.md)

</div>

What

A filesystem safety harness for DeepSeek Harness (DSH). It enforces a three-tier file policy at the tool-execution boundary and, crucially, makes the agent ask the human before it deletes or rewrites anything important:

  • destructive agent calls are denied before they run, with an educational

message explaining what the target is, why it matters, and what the sanctioned alternative is;

  • every delete is routed through a recoverable trash;
  • the plugin composition can be snapshotted and rolled back transactionally;
  • the composition is validated before a restart;
  • sensitive deletes/writes require a one-shot, time-limited user approval

the model can never authorize itself.

The package has zero runtime dependencies. It installs as a standard DSH profile bundle and also ships a standalone CLI, so the recovery and approval layer remains usable even 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 (rm -r/-rf, Remove-Item -Recurse, rd /s, rmdir, shutil.rmtree, fs.rm recursive, require('fs').rmSync…) are denied by default and routed to safe_delete (trash, undoable). In cooperative mode a human-granted approval can authorize a free-path recursive shell 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 unless the user has granted an approval. - Deletes on confirm zones (the whole OS home dir, plugin sources, agent presets) are denied and require a granted user approval. - 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).

  • Educational, anti-bypass denials — a blocked call returns why it was

blocked, what the target is, what the consequence would be, and what the sanctioned path is; the system prompt tells the agent to stop trying workarounds and ask the user instead; after repeated blocks for the same target the guard escalates with an explicit STOP.

  • User-gated approval systemsafety_ask creates a structured request

(what / why / consequence / alternative); the human approves via dsh-safety allow <id> (or dsh-safety delete --force); the approval is one-shot, time-limited, audited, and serialized across processes by an atomic lock. The model can never grant itself one — a force:true flag alone is not an approval. Every request also carries a system-computed consequence (from the real path classification) shown separately from the model's self-report, so the human approves on a system-backed verdict.

  • 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 transactionally (current files are backed up first; a failed rollback never leaves the composition half-restored). 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 — every block/approval/delete/snapshot/restore is

journaled and readable via safety_journal / safety_status.

  • Standalone CLIdsh-safety works without DSH: policy / delete /

undo / snapshot / restore / check / approve from your own terminal, even when DSH won't boot.

Approval workflow

The whole point of the approval system is that the model can never approve its own destructive calls. A flag it can set (force:true) is not a confirmation — only a human action (CLI) is.

agent calls delete/write on a confirm/protected zone
        │
        ▼
guard BLOCKS it (educational message: what / why / consequence / sanctioned path)
        │
        ▼
agent calls safety_ask { path, kind, what, why, consequence, alternative }
        │   → creates a request (the SYSTEM also computes an authoritative
        │     consequence from the real path classification), returns an id
        ▼
agent tells the user:  "please approve: dsh-safety allow <id>"
        │
        ▼
USER runs  dsh-safety allow <id>        (or: dsh-safety delete --force)
        │   → grants a one-shot, time-limited approval (default 5 min)
        ▼
agent retries the original call → guard consumes the approval and LETS IT THROUGH
        │   (the approval is now spent; a second call is blocked again)
        ▼
everything is audited: who requested, who approved, when, when it was consumed

Practical details:

  • Requesting: when a call is blocked, the denial message tells the agent

to call safety_ask with the causality. The request carries what/why/consequence/alternative and a system-computed systemNotedsh-safety approvals shows the [system] verdict and the (model: …) self-report separately, so you approve on the system's classification, not the model's unverifiable story.

  • Approving: dsh-safety allow <id> approves an agent-created request.

dsh-safety allow --path <p> --kind delete|write [--recursive] creates and

approves one directly (you are the human). dsh-safety delete --force on a confirm/protected path also grants the approval it needs and then moves the item to trash.

  • One-shot: an approval is consumed by the first matching call

(exact kind + exact target; recursive approvals are exact on the flag and may be target-agnostic). After that it is spent.

  • Time-limited: a granted approval expires after approvalTtlMs

(default 5 minutes) and must be re-granted.

  • Approved calls run as written — safe_delete is the trash channel: an

approved raw retry (e.g. a Remove-Item re-run) executes as-is; that is exactly what the human authorized. If you want the operation recoverable, have the agent use safe_delete (always trash, undoable) instead of raw shell. Recursive raw deletes on protected/confirm paths are never approvable via raw shell — they always go through safe_delete.

  • Strict vs cooperative: in mode: strict (default), raw recursive shell

deletes are never approvable — the only way to remove a directory tree is safe_delete (trash, undoable). In mode: cooperative, the human can authorize a free-path recursive shell delete with a generic recursive approval (dsh-safety allow --path … --recursive).

  • Anti-loop: if the agent retries the same blocked target repeatedly, the

guard escalates and tells it to stop and ask the user.

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.

From npm (recommended)

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/state.mjs/audit.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/@suagr_xl/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 approve/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      # free path — just works
dsh-safety delete path/to/file                # moves to trash (undoable)
dsh-safety delete path/to/important --force   # confirm/protected zone:
                                              #   --force IS the human approval here

# 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 + pending approvals
dsh-safety restore <snapshot-id> --confirm

# 6. Approve a request the agent created (model asked via safety_ask)
dsh-safety approvals
dsh-safety allow <request-id>

CLI reference

dsh-safety status                  state: trash, snapshots, approvals, 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 approvals               list pending/granted approval requests
dsh-safety allow <id>              approve a request the agent created
dsh-safety allow --path <p> [--kind delete|write] [--recursive]   approve a new one directly
dsh-safety revoke <id>             revoke a request
dsh-safety help

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

The plugin's configured roots live in the cordis patch layers, which a standalone CLI cannot read — so delete/policy accept the same overrides to align with the running guard:

--write-root <path>      add a protected (no write/edit/delete) root
--confirm-root <path>    add a confirm-delete (trash-only) root
--no-home-confirm        do NOT make the whole OS home a confirm zone
--keep-trash=N / --keep-snapshots=N    retention caps after delete/snapshot

> The CLI is the human side of the approval flow: dsh-safety delete --force > and dsh-safety allow are REAL user authorizations (recorded in state); the > model can never grant itself an approval.

Model-facing tools (when installed as a plugin)

ToolPurpose
safe_deletetrash-based delete (preview / user approval / undoable). force:true is NOT a user approval — the deletion needs a granted approval first
safety_askrequest the user's approval with the causality (what / why / consequence / alternative); the user approves via dsh-safety allow <id>
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 (incl. pending approvals)

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
    mode: strict            # strict | cooperative
    approvalTtlMs: 300000   # approval validity window (5 min default)
FieldDefaultMeaning
blockWriteRootsprofile manifests/patches/lockfiles/node_modules, install dir, home patch/settingsno write/edit/delete
confirmDeleteRoots$HOME, profiles/*, .agent-presetsno delete without a granted user approval (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
modestrictstrict: recursive shell deletes are never approvable; cooperative: the human can authorize them via the approval flow
approvalTtlMs300000how long a granted approval stays valid before it must be re-granted

How it works

Three-tier policy:

TierAllowedDeniedDefault coverage
protectedreadwrite / edit / delete (unless a user approval is granted)profile package.json/cordis.patch.yml/cordis.yml/lockfiles/node_modules, install dir, home patch & settings
confirmread, editdelete (needs a granted user approval)entire $HOME, plugin sources, agent presets
freeread/write/deleterecursive delete (approvable in cooperative mode)regular 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. A matching, granted user approval lets the call through once; otherwise it is denied.

Denials are educational: they name the target, describe it, explain the consequence (e.g. "rewriting this can make DSH fail to boot") and the sanctioned path (safe_delete / safety_ask), and tell the agent not to try workarounds. Repeated blocks for the same target escalate to an explicit STOP. 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. Approval state lives in $DSH_HOME/.dsh-safety/state.json, shared by the guard, `safe_del