Files
NCBM 0e8b0f8bd4 docs: describe deletes that go to the trash
A new page, `doc/trash.md`, covers what a delete does now: where a path lands on
each platform, what is still deleted outright (an explicit `permanent=true`, a
host that cleared `[trash]`, scratch paths under the OS temp dir, the
`copy_path` / `move_path` overwrite, the temporary-workspace cleanup), what
happens when a trashing is refused — the file stays and the error says how to
delete it on purpose — and the two things the trash cannot help with: a shell
`rm` and anything a plugin or MCP server does on its own.

`[trash]` joins the example config and the README snippet, the README gains the
one-paragraph version, `AGENTS.md` the tool/config notes (including the confirm
wording a delete now uses), and the default tool playbook tells the model that
`delete_path` is undoable and what `permanent=true` costs.
2026-10-21 20:25:27 +08:00

4.8 KiB

Deletes and the system trash

delete_path is the agent's only way to remove a path, and it is tagged YOLO: under --yes / /yolo on it runs with no confirmation at all. So the default has to be recoverable — a delete moves the path into the desktop's own trash, where the file manager the human already uses can put it back.

deleted file notes.md (moved to system trash)
deleted directory build (moved to system trash)
deleted file notes.md                               # permanent=true

permanent=true is the old behaviour, chosen on purpose. [trash] enabled = false makes every delete that way.

Where a path goes

Platform Trash
Linux / BSD The freedesktop trash specification: $XDG_DATA_HOME/Trash (~/.local/share/Trash by default), or a per-volume trash when the path is on another filesystem
macOS The Finder's trash (FSMoveObjectToTrashSync; NSFileManager when pyobjc is installed)
Windows The Recycle Bin (IFileOperation with pywin32, else SHFileOperation)

The platform work is send2trash, a required dependency because a missing safety net that fails silently is worse than no safety net. It is pure Python over ctypes — nothing to compile, no runtime dependencies.

Nothing is renamed away piecemeal: the whole path moves into the trash in one rename, so a large directory costs nothing to delete (across filesystems, where the volume has no trash of its own, the fallback is a move into the home trash, which does copy). A path trashed twice keeps both copies (dup.txt, then dup 1.txt).

What is deleted outright

Case Why
permanent=true The call said so
[trash] enabled = false The host said so
A path under the OS temporary directory Scratch space: nobody restores it, and a trash on the /tmp volume would only collect it. The same rule that new_temporary_workspace cleans up with
new_temporary_workspace cleanup at chat exit Same reason
copy_path / move_path with overwrite=true The destination is being replaced by an explicit call; the source content is the point of the operation

When the trash is unavailable

A trashing that fails is reported, never downgraded to a deletion — the human was told the path is recoverable, so guessing would be worse than stopping:

error: cannot move to trash: Permission denied (use permanent=true)
error: cannot move to trash: send2trash is not installed (use permanent=true)

The usual causes are a path on a filesystem with no .Trash directory that the user cannot create one on (network mounts, some FUSE filesystems, other people's volumes), a parent directory without write permission, and a broken install. In every case the path is untouched and the error names the way forward.

Restoring

Restore from the desktop's own trash — the file manager's "Trash" / "Recycle Bin", which is where a human would look anyway. plyngent does not offer a restore tool: send2trash does not report where a path landed, and the platform trash formats differ. A delete therefore reports that the path is in the trash, not where.

What the trash does not cover

  • A shell command. run_argv(["rm", "-rf", …]), a script, or anything run through a PTY reaches the filesystem directly; the command denylist and the soft confirm of interpreter runs are the only gates there, and rm does not use the trash.
  • Plugins and MCP servers. They bring their own handlers.
  • Writes. write_file with overwrite=true replaces a file's contents in place — it never removes a path. Only delete_path, the copy_path / move_path overwrite, and the temporary-workspace cleanup remove paths.

Privacy

A trashed path is still on disk until the trash is emptied — that is the point of it. [agent].path_denylist still runs first, so a denied path is never trashed either, and a trashed file has left the workspace (it is no longer in a git status, and no longer readable by a path tool).

Platform limits worth knowing

  • Windows: the shell deletes some things outright even when the call asked for the Recycle Bin — empty folders (a folder only reaches the bin with content), files above the bin's size limit for that drive, and paths on network shares. That is Windows' behaviour, not a fallback here.
  • Linux: a path on another volume needs a trash directory there (volume/.Trash/$UID with the sticky bit, or a creatable volume/.Trash-$UID). Without one the file is moved into the home trash instead, which crosses a filesystem boundary and therefore copies.
  • macOS: without pyobjc the library calls the older FSMoveObjectToTrashSync; installing send2trash[nativeLib] switches it to NSFileManager. Both trash; neither needs anything from plyngent.