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
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-trash from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.