Between September and November this year, several incidents were made public in which a coding agent deleted user files or killed unrelated processes. They came from Claude Code, Codex, Antigravity and opencode alike. Most write-ups stop at "the agent ran rm -rf", but when you look at the actual commands, the agent almost never set out to delete something broad. In most cases a command meant to remove one narrow folder ended up pointing somewhere else because of shell evaluation order, quote handling, the return value of a path tool, or a link.
This post covers only the cases where the executed command can be confirmed from the original source (GitHub issues, Reddit posts and their attached screenshots). For each one it explains why the command did something other than what was intended, and what form to use when doing the same job. Where a cause could be reproduced, I reproduced it on macOS and Windows 11 (Windows PowerShell 5.1, Git for Windows 2.52, WSL2), and the results are noted in each case. The reproductions used echo and throwaway test folders instead of real deletion targets.
Summary
| Case | Agent | Command that ran | What actually happened | Safer form |
|---|---|---|---|---|
| claude-code#109735 | Claude Code (Windows, WSL) | wsl -- bash -lc '...R=~/dir; rsync -a --delete src/ $R/' | Outer shell expanded $R to empty, rsync synced to / and deleted files under /mnt/d | wsl --exec or a script file inside WSL, "${R:?}/" |
| r/google_antigravity | Antigravity (Windows) | cmd /c "rmdir /s /q \"D:\My Documents\...\profile\"" | cmd received rmdir /s /q \ and deleted from the root of D: | Remove-Item -LiteralPath '...' -Recurse -Force |
| codex#43343 | Codex CLI (Windows) | cmd.exe /d /c "rd /s /q \"\\?\$resolved\"" | Deleted several repositories under the volume root instead of the build folder | Same as above; build the cmd string in single quotes and print it first |
| claude-code#95426 | Claude Code (Windows, Git Bash) | rm -rf "$(cygpath -u 'C:\')" 2>/dev/null | Deleted all of /c/, about 600GB | Put the target in a variable, print it, stop if it is a root |
| opencode#62825 | opencode (Windows) | $home = Join-Path ...; Remove-Item -Recurse -Force (Split-Path $home) | Assignment to $home failed, C:\Users was deleted | Avoid automatic variable names, set $ErrorActionPreference = 'Stop' |
| r/ClaudeCode | Claude Code (macOS) | find output piped through xargs -n1 dirname into xargs rm -rf | Path split at a space, ~/Library deleted | -print0 with xargs -0, or find -exec rm -rf -- {} + |
| codex#42355 | Codex CLI (Windows) | git clean -fdX -- config/<subpath> | Deleted the whole ignored parent folder config/ | Check first with git clean -ndX |
| r/ClaudeAI | Claude Code (Windows) | git worktree remove --force <worktree> | Deleted a model folder of about 230GB that a junction inside the worktree pointed to | Remove the link first, then remove without --force |
| claude-code#107274 | Claude Code (macOS) | rm -rf (including .git) | Lost more than 12 months of git history | git worktree remove; move .git aside |
| claude-code#109627 | Claude Code (macOS) | pkill -f "gcloud.py compute" -u <user> --signal 0 | Killed every other Claude Code session and about 66 apps | pgrep -f to check existence, kill by PID |
The sections below group the cases by type of root cause rather than by agent, because the same cause showed up in nearly the same form across different agents.
1. When a command passes through two shells, the outer one interprets it first
A variable passed through WSL (claude-code#109735)
In #109735, filed on November 14, a subagent working on Windows ran the following command to make a WSL copy of the project folder. The user had left it running overnight without approvals.
wsl -d Ubuntu-24.04 -- bash -lc 'set -u; R=~/some-dir; mkdir -p $R; rsync -a --delete --exclude ... /mnt/d/<project>/ $R/; ...' The agent expected $R to be expanded by the inner bash, but wsl.exe hands the arguments after -- to the default Linux shell, which interprets them once more. That outer shell has no R, so $R becomes an empty string, and the command the inner bash receives is rsync -a --delete /mnt/d/<project>/ /. set -u is a setting of the inner bash, so it could not help once the expansion had already happened. rsync deleted the files under / that a normal user could delete, and that included the Windows data drive mounted at /mnt/d. Files deleted from WSL do not go to the Recycle Bin.
Testing the same structure with echo on WSL2 under Windows 11 shows the difference directly.
wsl -- bash -lc 'R=hello; echo "[R=$R]"' # [R=] wsl --exec bash -lc 'R=hello; echo "[R=$R]"' # [R=hello] --exec runs the program directly with no intermediate shell, so the quoted content reaches the inner bash unchanged. A more reliable approach is to put the script in a file inside WSL and run that file. If the target of a delete or a --delete sync is a variable, write it as "${R:?}/" so the shell stops with an error when the value is empty, and start with rsync -n to see what would be deleted.
Two more things in this issue are worth recording beyond the command itself. When the parent agent stopped the subtask, wsl.exe on the Windows side exited, but rsync inside WSL kept running for about two more minutes. And the subagent's first damage report was that the documents folder was unaffected. rsync does not print the files it deletes successfully, so the absence of error lines proves nothing.
Quotes passed from PowerShell to cmd (Antigravity, codex#43343)
A post on r/google_antigravity on November 6 (651 points) includes a screenshot of the explanation Antigravity itself wrote up after the incident. To delete D:\My Documents\Projects\_scratch_sitevideo\profile, the agent ran this from PowerShell:
cmd /c "rmdir /s /q \"D:\My Documents\Projects\_scratch_sitevideo\profile\"" PowerShell's escape character is the backtick, not the backslash. So the string ends at the quote in \", and the first argument becomes rmdir /s /q \. The rest is split again at the space in My Documents. Replacing rmdir with echo in Windows PowerShell 5.1 to see what cmd receives gives:
rmdir /s /q \ D:\My Documents\Projects\_scratch\profile\ In cmd, a lone \ is the root of the current drive. According to the screenshot, the deletion went through the top-level folders of D: in alphabetical order, and in the roughly 19 seconds before the agent noticed a permission error on $RECYCLE.BIN and stopped, the Epic Games folder (about 9GB) was deleted. The poster was reviewing commands one at a time, and wrote that they approved this one because at a glance it did not look broad.
#43343, filed in the Codex repository on September 7, has the same cause. Deleting a build folder with Remove-Item failed on a file with the reserved name NUL, so Codex switched to cmd with a \\?\ prefixed path.
cmd.exe /d /c "rd /s /q \"\\?\$resolved\"" The agent had just checked that $resolved matched the build folder, but that value did not reach cmd intact. The reporter confirmed through the NTFS USN journal that several repositories outside the working folder were deleted, and wrote that deletion continued for several minutes after Codex reported that it had stopped.
When deleting a folder from PowerShell, the simplest approach is to not go through cmd at all.
Remove-Item -LiteralPath 'D:\My Documents\Projects\_scratch\profile' -Recurse -Force -LiteralPath also does not interpret wildcard characters such as square brackets. If you must use cmd, build the string for cmd in single quotes and print what cmd receives with echo once before running it. In the test setup above, cmd /c 'echo rmdir /s /q "D:\My Documents\Projects\_scratch\profile"' passed the path as one piece. When a variable has to go in, build the string first, as in $line = 'rd /s /q "\\?\' + $resolved + '"', print it, and then run cmd /d /c $line. That way you can see where the shell will split it.
2. The computed path was a root
A drive root returned by cygpath (claude-code#95426)
In #95426, on September 16, Claude Code in Git Bash on Windows ran the following command, which the user had not asked for, with the stated reason of cleaning up stale test state.
rm -rf "$(cygpath -u 'C:\')" 2>/dev/null cygpath -u 'C:\' returns /c/ (confirmed with the cygpath that ships with Git for Windows). The value is not empty, so ${VAR:?} would not catch it, and quoting does not change the result. Because stderr was discarded, the permission errors that system folders should have produced were never seen, and about 600GB was deleted over about 35 minutes. ~/.claude/projects, which held the session logs, was deleted too, so the original session did not survive.
As the reporter pointed out, even if a permission prompt had appeared, what it shows is the expression $(cygpath ...), not the result /c/. When deleting by a computed path, add a separate step: put it in a variable, print it, and stop if it is a root or home.
T="$(cygpath -u "$WIN_PATH")" echo "Delete target: $T" case "$T" in /|/?|/?/|"$HOME"|"$HOME"/) echo "Refusing, root or home: $T" >&2; exit 1 ;; esac rm -rf -- "${T:?}/build" If you append a fixed subfolder name as in the last line, then even if the earlier computation returns a root, the deletion is limited to that subfolder. Do not add 2>/dev/null to delete commands.
An unassigned PowerShell automatic variable (opencode#62825)
In opencode#62825, the agent ran these two lines to clean up a temp folder.
$home = Join-Path $env:TEMP "qml-shot\home" Remove-Item -Recurse -Force (Split-Path $home) -ErrorAction SilentlyContinue PowerShell variable names are case-insensitive, so $home is the automatic variable $HOME, which is read-only. The assignment on the first line fails, but that error does not stop the script. Checking in Windows PowerShell 5.1: after $home = "X", the next line still runs, $home is C:\Users\<user>, and Split-Path $home returns C:\Users. So the second line recursively deleted C:\Users, and -ErrorAction SilentlyContinue hid the errors for files that were in use and could not be deleted. The reporter wrote that their opencode config, ~/.cargo/bin, ~/.bun and ~/.local/bin were deleted, and that because opencode recreated its config folder on the next run, they first mistook it for an update problem.
Do not use automatic variable names such as $home, $profile, $input, $args, $pwd or $host as temporary variables, and put $ErrorActionPreference = 'Stop' on the first line of cleanup scripts so execution stops at the failed assignment. Do not add -ErrorAction SilentlyContinue to delete commands.
3. A path passed as text was split again
xargs and Application Support (r/ClaudeCode)
In a post on r/ClaudeCode on November 7 that passed 4,000 points, ~/Library on macOS was deleted. The agent was trying to delete five test channels it had created earlier, and those files lived under ~/Library/Application Support/. The agent computed the parent folders with xargs -n1 dirname and passed them to rm -rf. The poster wrote that they were running with --dangerously-skip-permissions and that the deletion ran for about two minutes while they were away.
By default, xargs splits input on spaces and newlines. Here is the result with a test folder of the same shape on macOS:
$ find . -path '*ch1' | xargs -n1 dirname ./Library Support/app $ find . -path '*ch1' -print0 | xargs -0 -n1 dirname ./Library/Application Support/app The path is split in two at the space in Application Support, and dirname of the first piece is Library. When passing paths to another command, pair -print0 with xargs -0, and when deleting, it is better to let find do it directly with no pipe.
find "$HOME/Library/Application Support/myapp" -maxdepth 1 -type d -name 'test-*' -exec rm -rf -- {} + There is one more point in this case. What needed deleting was five channels, but the command was shaped to delete their parent folder. When cleaning up things an agent created itself, recording the paths at creation time and deleting only those paths is narrower than computing a parent folder.
4. The command's scope was wider than expected
git clean removing an entire ignored parent folder (codex#42355)
In codex#42355, the user asked to delete only a few unused subfolders under config/, and Codex ran git clean -fdX -- config/<subpath>. config/ was listed in .gitignore, and git deleted all of config/. It contained API key files and operational scripts, and they were not included in the partial backup the agent had made.
Building the same structure with git 2.56 reproduces this behavior exactly. Even when you specify a path inside an ignored folder, git treats that folder as a single unit, and the dry run already shows it will remove config/.
$ git clean -ndX -- config/skills/old Would remove config/ Always run git clean with -n first to see the list, and stop if a folder above the path you specified shows up. If the job is to delete a few folders, specifying those paths directly is clearer in scope than git clean.
A junction inside a worktree (r/ClaudeAI)
In a post on r/ClaudeAI on November 10, a real model folder (about 230GB) and a .venv were deleted while two old git worktrees were being cleaned up on Windows. The agent explained that it had checked whether the worktrees' remaining changes had been merged into the main branch, but had not checked whether the worktrees contained directory junctions pointing at real folders. The poster stated that this is the agent's own explanation and not an independently confirmed cause.
I built the same structure with test folders on Windows 11 and Git for Windows 2.52. With a junction inside the worktree pointing at an outside folder, running git worktree remove --force deleted the files inside the folder the junction pointed to. In the same state without --force, git refused with "contains modified or untracked files". After first removing the junction with cmd /c rmdir <junction> (without /s), the worktree was removed normally without --force and the target folder survived. For reference, in the same environment, Remove-Item -Recurse -Force in Windows PowerShell 5.1 did not delete files beyond the junction. Whether a tool follows links differs by tool and version, so rather than assuming some tool is safe, it is more reliable to find and remove the links first.
cmd /c dir /AL /S "D:\project-wt-old" # list junctions and symlinks cmd /c rmdir "D:\project-wt-old\models" # remove only the link, no /s git worktree remove "D:\project-wt-old" # without --force A message saying --force is required means something is still left in the worktree. When you see it, look at what is left before forcing anything.
The .git folder (claude-code#107274)
#107274, reported on macOS, is short. It says more than 12 months of git history were lost to a mistaken rm -rf, and the exact command was not made public. The lesson is still clear: files in the working tree can be recreated, but if .git is deleted, unpushed commits, branches and stashes go with it.
To clean up a checkout, use git worktree remove. If you need to recreate a repository, do not delete .git; move it aside, as in mv .git ../repo.git.bak. For a repository with no remote, git bundle create ../repo-backup.bundle --all keeps the entire history as a single file outside the repository.
5. How a process-kill command parsed its arguments (claude-code#109627)
In #109627, the target was processes rather than files. On macOS, a subagent in a workflow ran this to check whether a gcloud process was still alive:
pkill -f "gcloud.py compute" -u <user> --signal 0 --signal 0 is Linux procps syntax, and signal 0 only checks for existence. The BSD pkill on macOS has no long options, so it read --signal and 0 as additional patterns and sent the default signal, SIGTERM, to every process matching any of the patterns. 0 appears in almost every command line. As a result, about 66 processes were terminated, including every Claude Code session other than this one, the Claude desktop app, Slack and Chrome helpers. The instructions given to the agent even included a sentence telling it not to kill gcloud.
Testing the same arguments on macOS with pgrep, which only lists, pgrep -f "<nonexistent name>" -u "$USER" --signal 0 returns more than 400 processes even for a name that does not exist. This is because BSD argument parsing does not treat arguments after the first pattern as options. To check existence, use pgrep -f 'gcloud.py compute', and if termination is needed, read the PID from that output and end it with kill <pid>. On macOS, always put options before the pattern.
What repeated across the cases
The commands differed, but a few things came up in several cases.
First, most were cleanup tasks: cleaning up test state (#95426), a temp folder (opencode), a build folder (codex#43343), test channels (r/ClaudeCode), a temporary browser profile (Antigravity), and old worktrees (r/ClaudeAI). These were steps the agent added on its own before or after the main task, and in some cases the user had not asked for them.
Second, options that hide errors made the damage worse. 2>/dev/null (#95426) and -ErrorAction SilentlyContinue (opencode) suppressed permission errors, which are the first signal that a delete has started on the wrong target.
Third, deletion continued after stopping, and the first damage report was wrong. In #109735, rsync inside WSL ran for about two more minutes after the task was stopped, and in codex#43343, deletion continued for several minutes after Codex reported that it had stopped. The "unaffected" report in #109735 was found to be wrong a few hours later.
Fourth, 7 of the 10 cases were on Windows. At the center of the cause was a setup in which a command is reinterpreted one or more times as it passes between PowerShell, cmd, Git Bash and WSL.
What helps
Command habits
Collecting the forms from the cases above: when deleting through a variable, use "${VAR:?}/fixed-name" so an empty value stops the shell and a fixed subfolder name narrows the scope. Print computed paths before deleting, and stop if they are a root or home. When passing paths through a pipe, use -print0 | xargs -0 or handle them with find -exec. In PowerShell, use Remove-Item -LiteralPath without going through cmd, avoid automatic variable names, and set $ErrorActionPreference = 'Stop'. When passing commands into WSL, use wsl --exec or a script file. In folders that may contain links, remove the links first, and run git worktree remove and git clean without --force, or with -n first. Do not add error-hiding options to deletes or --delete syncs. You can write these habits into the agent's instruction file (CLAUDE.md, AGENTS.md and so on), but as #109627 shows, sentences in the instructions can still be violated, so instructions alone are hard to call sufficient.
Permission prompts
Making the agent ask for confirmation on every command lets a person see and stop many incidents. These cases also showed the limits, though. As the #95426 reporter pointed out, the prompt shows the expression $(cygpath ...), not its result. The Antigravity poster reviewed the command and still approved it, and commenters said they would not have noticed even if a prompt had appeared. In a reply on the issue, an opencode maintainer explained that permission rules match only the command string, so they cannot catch paths computed at run time or deletions inside script files. Claude Code checkpoints, according to the official docs, do not revert files changed only by Bash commands.
Sandboxes and virtual machines
A sandbox that limits writes to the working folder reduces the blast radius no matter where a computation points. Claude Code's sandbox allows writes only to the working folder and temp folders on macOS, Linux and WSL2, but according to the same docs, commands run without a sandbox when running natively on Windows. That is worth reading alongside the fact that many of these cases were on Windows. A virtual machine, a container, or a separate low-privilege account has the same effect, but if a host drive is mounted writable at /mnt/d as in #109735, that drive is not outside the isolation boundary.
Backups outside the repository
Whatever mistake an agent makes, the last line of defense is a backup. The r/ClaudeCode poster wrote that they had no backup (and later added that they recovered most of the important data). The codex#42355 user restored the API keys from a backup they had made separately, and the codex#43343 reporter recovered most of it from the remote repository and the IDE's local history. You need a copy somewhere other than the folder the agent works in: a remote push, git bundle, or Time Machine on macOS (right after a deletion, tmutil listlocalsnapshots / shows local snapshots).
Pre-execution hooks
Claude Code, Codex, Gemini CLI and others support hooks that call a user script before a tool runs, where the command can be read and blocked. destructive_command_guard (dcg), recommended in the comments of the Antigravity post, is a hook written in Rust that blocks destructive filesystem, git, database and other commands, and can be wired into several agents. The #95426 reporter also wrote that after the incident they built their own hook that asks for confirmation on every recursive delete, and the issue comments introduce several hooks made by others.
Hooks have limits too. Most read the command string, so they cannot see commands inside script files the agent runs, and they cannot know values computed at run time. As a comment on #95426 pointed out, a hook cannot block based on the result of $(cygpath ...); the only option is to block the rm -rf action itself, and confirmation requests grow accordingly. So a hook is best seen as one of several layers, used together with the habits, sandboxes and backups above.
I maintain one such pre-execution hook, claude-bulkhead, and several of the cases in this post were the starting point for its rules.
![Software Architecture Fundamentals — Ch.09: Scalable System Design [KR]](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fulk272oee9h5w3aozfmo.png)
![Software Architecture Fundamentals — Ch.10: Software Defined Robot Architecture [KR]](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkgatq0r1ijgi590hux6o.png)
![Software Architecture Fundamentals — Ch.11: Cross Platform SW Design [KR]](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuj6eszp10nmqzkcrqxcn.png)
