dsh-trash
English | 中文
A DeepSeek Harness plugin that turns every delete operation into a recoverable trash move — even under the highest permission (danger-full-access). Accidentally deleted something? Restore it in one call.
- Intercepts delete commands in the
pwsh/bash/run_codetools (Remove-Item,rm,del,rd,rmdir,unlink,[System.IO.*]::Delete,fs.unlink/rm/rmdir,os.remove,shutil.rmtree, …) at the tool pipeline'stools/pre-executegate, denies them, and guides the model to the trash tools. - Ships 4 tools:
trash(delete = move to trash),trash-list(inspect),trash-restore(recover),trash-purge(confirmed permanent cleanup — the only path that truly deletes). - Zero runtime dependencies (Node built-ins only), installable from anywhere (registry /
file:/link:/ GitHub).
How it works
| Layer | What happens |
|---|---|
| Interceptor | A tools/pre-execute listener (prepended, so it runs first) scans pwsh.command, bash.command, and run_code.code for delete patterns. On a hit it returns { kind: "deny" } — denials are monotonic, so no later stage (approval, guard, dispatch) can let the call through. The model sees an actionable message telling it to use trash instead. |
| Trash store | Each "delete" becomes one entry under the trash root (default $DSH_HOME/trash): data/ keeps the original names, meta.json records original paths, timestamps, sizes, and an optional reason. Cross-volume moves degrade to copy-then-clean. |
| Tools | trash / trash-list / trash-restore / trash-purge — see below. |
<trashRoot>/
<entryId>/
data/... the "deleted" files & directories (original names preserved)
meta.json original path, deleted-at, size, reason
Installation
# From a local checkout (dev / link mode — edits to the source take effect immediately)
dsh plugin --profile web add link:D:/path/to/dsh-trash
# Or straight from GitHub (requires git + a GitHub token on this machine)
dsh plugin --profile web add github:LeonSone/dsh-trash
dsh plugin add adds the package to the profile's dependencies; because the package declares dsh.bundle, it is automatically appended to dsh.profile.bundles. Restart dsh web (or the headless runner) for the plugin to load.
Headless profile:
dsh plugin --profile headless add <spec>— same flow.
Usage
Once installed, the model's behavior changes automatically:
| Scenario | Behavior |
|---|---|
Model runs Remove-Item / rm / del / … |
Blocked with a message explaining the trash workflow |
Model calls trash(paths=[...]) |
Files/dirs move into the trash (recoverable) |
| Accidental delete | trash-restore(entry_id=...) restores to the original path; or trash-restore(path="<substring of original path>") finds the newest matching entry |
| Permanent cleanup | trash-purge(confirm: true) — optionally scoped to specific entry_ids |
Tool reference
| Tool | Purpose |
|---|---|
trash(paths, reason?) |
Move files/directories into the trash. Returns entry_id, trash_root, moved. |
trash-list(path?) |
List trash entries (deleted-at, size, reason, original paths). Optional substring filter on the original path. |
trash-restore(entry_id? | path?, target_path?, overwrite?) |
Restore an entry to its original location (or target_path for single-item entries). If the target exists, it refuses unless overwrite: true — in which case the existing content is first moved to the trash itself, then the restore proceeds. Nothing is ever lost. |
trash-purge(entry_ids?, confirm: true) |
Permanently delete trash entries. The only real deletion in the whole plugin. |
Configuration (optional)
Append to your profile's cordis.patch.yml:
- id: dsh-trash
config:
location: D:/data/dsh-trash # trash root; same drive as the workspace = instant renames
interceptPwsh: true # intercept deletes in pwsh (default true)
interceptBash: true # intercept deletes in bash (default true)
interceptRunCode: true # intercept fs.* deletes in run_code (default true)
Security notes
- Fail-safe by design: the detector prefers false positives over letting a real delete through (e.g. a tutorial string mentioning
Remove-Itemis blocked too; the denial message explains why). Short aliases (rm/del/rd/ri) only match in command position, socat /tmp/rm,Test-Path C:\rd, orgrep 'rm -rf' docsare not flagged. - One true delete path: only
trash-purge(confirm: true), and it only touches the trash root. - Restore safety:
entry_idis strictly validated (no path traversal);target_pathmay not point inside the trash root. - Overwrite safety: restoring over an existing file first backs that file up into the trash — every step is reversible.
Development
node test/unit.test.mjs # 48 cases: detector hit/miss matrix + full trash-store workflow
Uninstall
dsh plugin --profile web remove dsh-trash
The trash directory itself is left untouched; move it away first if you want to keep the data.
No comments yet. Be the first to write one.