Bundle
dsh-safe-delete
Intercepts `rm` commands issued by any DSH agent session (GUI, automation runs, headless bridges) at the tools guard layer and moves the targets to the macOS Trash instead of deleting them. Switchable from the web GUI under Settings → General.
- Source
- NattoCB
- License
- MIT
- Updated
- Updated yesterday
Readme
# dsh-safe-delete
<!-- Hero -->
<div align="center">
<b style="font-size: 1.15em;">Every <code>rm</code> an agent runs becomes a recoverable move to the macOS Trash.</b><br /><br />
<a href="LICENSE"><img alt="License" src="https://img.shields.io/badge/License-MIT-yellow.svg" /></a>
<img alt="Platform" src="https://img.shields.io/badge/platform-macOS-black" />
<img alt="Version" src="https://img.shields.io/badge/version-0.2.0-blue" />
<a href="https://github.com/awesome-dsh-plugin/awesome-dsh-plugin"><img alt="DeepSeek Harness" src="https://img.shields.io/badge/DeepSeek%20Harness-plugin-4d6bfe" /></a><br /><br />
<img alt="rm → Trash" src="https://img.shields.io/badge/-rm_%E2%86%92_Trash-4d6bfe" />
<img alt="Global Tools Guard" src="https://img.shields.io/badge/-Global_Tools_Guard-4d6bfe" />
<img alt="Settings Switch" src="https://img.shields.io/badge/-Settings_Switch-4d6bfe" />
<img alt="Shell-aware Lexer" src="https://img.shields.io/badge/-Shell_aware_Lexer-4d6bfe" />
<img alt="Zero Dependencies" src="https://img.shields.io/badge/-Zero_Dependencies-4d6bfe" /><br /><br />
<b>One global guard on the tools registry — <code>ctx.tools.guard()</code></b> —— no tool, prompt, or core changes
</div>
> A [DeepSeek Harness](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin) host-side plugin that makes agent-issued `rm` commands recoverable: targets are **moved to the macOS Trash** instead of deleted, across GUI sessions, automation runs, headless bridges, and subagents. One switch in **Settings → General** turns it off — disabled, commands run with native DSH behavior.
## Capabilities
- **Recoverable deletion** — an `rm` in command position is denied and rewritten to `/usr/bin/trash -v`; the model receives a report of exactly what was moved, so nothing silently disappears.
- **Every session, one guard** — registers once on the DSH tools registry; GUI chats, scheduled automations, headless bridges, and subagents are all covered with zero per-session wiring.
- **Shell-aware interception** — quote-preserving lexer catches `sudo rm`, `/bin/rm`, `env`/`nice`-prefixed `rm`, `VAR=x rm`, and `xargs rm` inside compound commands (`&&`, `||`, `;`, `|`).
- **Fail-safe by design** — constructs that could hide an `rm` (`$(...)`, backticks, subshells, heredocs, `eval`, nested `sh -c`) are denied with guidance instead of rewritten; a failed trash is reported, never retried as real deletion.
- **Runtime switch** — toggle in the web GUI (Settings → General → Safe Delete) or via `POST /safe-delete/config`; the state persists across restarts in `$DSH_HOME/storages/safe-delete.json`.
## How it works
The plugin installs one guard on the DSH tools registry. Every `bash` tool call is scanned before execution:
```mermaid
flowchart LR
A["bash tool call"] --> B{"rm in command<br/>position?"}
B -- "no" --> C["native execution"]
B -- "yes" --> D{"switch on?"}
D -- "off" --> C
D -- "on" --> E{"safely<br/>rewritable?"}
E -- "no: subshell, eval,<br/>heredoc, backticks" --> F["deny with guidance,<br/>nothing runs"]
E -- "yes" --> G["run trash -v, deny the<br/>original, report targets"]
```
Intercepted calls are denied and re-executed as trash by the guard itself, so quoting and globs survive:
```text
[dsh-safe-delete] intercepted `rm` — the targets were MOVED TO TRASH (recoverable),
not deleted. Original command denied. Targets: build/ dist
trash: build/ → .Trash/build/
```
Plain non-`rm` commands pass through untouched; commands that merely *mention* `rm` (`echo rm`, `grep "rm " log`) are ignored. `rm` flags (`-f`, `-r`, …) are stripped rather than honored: `trash` moves whole directories natively.
## Scope and limits
| | Covered | Not covered |
|---|---|---|
| Commands | `rm`, `sudo rm`, absolute-path `rm`, `xargs rm` — simple and compound | `find -delete`, `unlink`, `git clean`, language runtimes' own file APIs |
| Guarantee | intercepted at the guard layer, before execution | the guard is an accident-prevention net for the most common destructive verb, not a sandbox |
`sudo rm` is intercepted before `sudo` ever runs, but trashing files that require root can still fail — trash errors are reported verbatim.
## Install
```bash
dsh plugin --profile web add github:NattoCB/dsh-safe-delete
```
Restart the DSH web process — host-side bundles and their patches load at process start. On boot the plugin logs `rm guard active` once.
## Configuration
| Surface | How |
|---|---|
| Web GUI | **Settings → General → Safe Delete** — the switch takes effect on the next `bash` call, no reload |
| HTTP | `GET /safe-delete/config` → `{ "enabled": bool }`; `POST /safe-delete/config` with `{ "enabled": bool }` |
| File | `$DSH_HOME/storages/safe-delete.json` — `{ "enabled": bool }` |
**Default: enabled.** A missing or corrupt state file also resolves to enabled — installing the plugin expresses the intent to have the guard, and a corrupt file never silently downgrades protection.
## Development
```bash
npm test # node --test: lexer, rewrite matrix, guard contract, switch store/API
```
The lexer/rewriter is exported as `exports._internals` so tests exercise the real code paths (`analyzeCommand`, `tokenize`, `splitSegments`, `makeGuard`, `ConfigStore`, `handleConfigRequest`).
## License
[MIT](LICENSE) — part of the [awesome-dsh-plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin) ecosystem.
Install
dsh plugin --profile web add github:NattoCB/dsh-safe-delete
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-safe-delete from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.