Skip to content
dsh.fish
Bundle

dsh-trash

DSH 回收站插件:最高权限下所有删除操作一律拦截并转入回收站,误删可一键恢复。Intercepts every delete operation (Remove-Item/rm/del/fs.unlink/...) and redirects it to a recoverable trash store.

Source
LeonSone
License
MIT
Updated
Updated 6 days ago

Readme

# dsh-trash

**English** | [中文](README.zh-CN.md)

A [DeepSeek Harness](https://github.com/deepseek-ai/dsh) 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_code` tools (`Remove-Item`, `rm`, `del`, `rd`, `rmdir`, `unlink`, `[System.IO.*]::Delete`, `fs.unlink/rm/rmdir`, `os.remove`, `shutil.rmtree`, …) at the tool pipeline's `tools/pre-execute` gate, 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

```powershell
# 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`:

```yaml
- 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-Item` is blocked too; the denial message explains why). Short aliases (`rm`/`del`/`rd`/`ri`) only match in *command position*, so `cat /tmp/rm`, `Test-Path C:\rd`, or `grep 'rm -rf' docs` are **not** flagged.
- **One true delete path**: only `trash-purge(confirm: true)`, and it only touches the trash root.
- **Restore safety**: `entry_id` is strictly validated (no path traversal); `target_path` may 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

```powershell
node test/unit.test.mjs   # 48 cases: detector hit/miss matrix + full trash-store workflow
```

## Uninstall

```powershell
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.

## License

[MIT](LICENSE)

Install

dsh plugin --profile web add github:LeonSone/dsh-trash

Profile: web

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