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.
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, andrmdoes not use the trash. - Plugins and MCP servers. They bring their own handlers.
- Writes.
write_filewithoverwrite=truereplaces a file's contents in place — it never removes a path. Onlydelete_path, thecopy_path/move_pathoverwrite, 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/$UIDwith the sticky bit, or a creatablevolume/.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; installingsend2trash[nativeLib]switches it toNSFileManager. Both trash; neither needs anything from plyngent.