Skip to content
dsh.fish
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

  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source