{"slug": "ten-coding-agent-deletions-traced-to-the-shell-command-that-ran", "title": "Ten coding-agent deletions, traced to the shell command that ran", "summary": "A developer catalogued ten publicly documented incidents in which coding agents — including Claude Code, Codex CLI, Antigravity and opencode — deleted user files or killed unrelated processes, tracing each to the exact shell command that ran. The analysis found the agents rarely intended broad deletion; instead, shell evaluation order, quote handling, path-tool return values and symlinks or junctions redirected narrow commands at volume roots, home directories and whole repositories. The author reproduced several causes on macOS and Windows 11 using echo and throwaway folders, and paired each case with a safer command form such as \"${R:?}\", -print0 with xargs -0, or git clean -ndX dry runs.", "body_md": "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.\n\nThis 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.\n\n| Case | Agent | Command that ran | What actually happened | Safer form | \n|---|---|---|---|---|\n| [claude-code#109735](https://github.com/anthropics/claude-code/issues/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:?}/\"` | \n| [r/google_antigravity](https://www.reddit.com/r/google_antigravity/comments/1xpbc54/) | 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` | \n| [codex#43343](https://github.com/openai/codex/issues/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 | \n| [claude-code#95426](https://github.com/anthropics/claude-code/issues/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 | \n| [opencode#62825](https://github.com/anomalyco/opencode/issues/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'` | \n| [r/ClaudeCode](https://www.reddit.com/r/ClaudeCode/comments/1xqfp7m/) | 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 -- {} +` | \n| [codex#42355](https://github.com/openai/codex/issues/42355) | Codex CLI (Windows) | `git clean -fdX -- config/<subpath>` | Deleted the whole ignored parent folder `config/` | Check first with `git clean -ndX` | \n| [r/ClaudeAI](https://www.reddit.com/r/ClaudeAI/comments/1xsgltl/) | 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` | \n| [claude-code#107274](https://github.com/anthropics/claude-code/issues/107274) | Claude Code (macOS) | `rm -rf` (including`.git` ) | Lost more than 12 months of git history | `git worktree remove` ; move`.git` aside | \n| [claude-code#109627](https://github.com/anthropics/claude-code/issues/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 | \n\nThe 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.\n\nIn [#109735](https://github.com/anthropics/claude-code/issues/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.\n\n```\nwsl -d Ubuntu-24.04 -- bash -lc 'set -u; R=~/some-dir; mkdir -p $R; rsync -a --delete --exclude ... /mnt/d/<project>/ $R/; ...'\n```\n\nThe 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.\n\nTesting the same structure with `echo` on WSL2 under Windows 11 shows the difference directly.\n\n```\nwsl -- bash -lc 'R=hello; echo \"[R=$R]\"'         # [R=]\nwsl --exec bash -lc 'R=hello; echo \"[R=$R]\"'     # [R=hello]\n```\n\n`--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.\n\nTwo 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.\n\nA [post](https://www.reddit.com/r/google_antigravity/comments/1xpbc54/) 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:\n\n```\ncmd /c \"rmdir /s /q \\\"D:\\My Documents\\Projects\\_scratch_sitevideo\\profile\\\"\"\n```\n\nPowerShell'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:\n\n```\nrmdir /s /q \\ D:\\My Documents\\Projects\\_scratch\\profile\\\n```\n\nIn 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.\n\n[#43343](https://github.com/openai/codex/issues/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.\n\n```\ncmd.exe /d /c \"rd /s /q \\\"\\\\?\\$resolved\\\"\"\n```\n\nThe 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.\n\nWhen deleting a folder from PowerShell, the simplest approach is to not go through cmd at all.\n\n```\nRemove-Item -LiteralPath 'D:\\My Documents\\Projects\\_scratch\\profile' -Recurse -Force\n```\n\n`-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.\n\nIn [#95426](https://github.com/anthropics/claude-code/issues/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.\n\n```\nrm -rf \"$(cygpath -u 'C:\\')\" 2>/dev/null\n```\n\n`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.\n\nAs 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.\n\n```\nT=\"$(cygpath -u \"$WIN_PATH\")\"\necho \"Delete target: $T\"\ncase \"$T\" in\n  /|/?|/?/|\"$HOME\"|\"$HOME\"/) echo \"Refusing, root or home: $T\" >&2; exit 1 ;;\nesac\nrm -rf -- \"${T:?}/build\"\n```\n\nIf 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.\n\nIn [opencode#62825](https://github.com/anomalyco/opencode/issues/62825), the agent ran these two lines to clean up a temp folder.\n\n``` php\n$home = Join-Path $env:TEMP \"qml-shot\\home\"\nRemove-Item -Recurse -Force (Split-Path $home) -ErrorAction SilentlyContinue\n```\n\nPowerShell 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.\n\nDo 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.\n\nIn a [post](https://www.reddit.com/r/ClaudeCode/comments/1xqfp7m/) 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.\n\nBy default, xargs splits input on spaces and newlines. Here is the result with a test folder of the same shape on macOS:\n\n``` bash\n$ find . -path '*ch1' | xargs -n1 dirname\n./Library\nSupport/app\n$ find . -path '*ch1' -print0 | xargs -0 -n1 dirname\n./Library/Application Support/app\n```\n\nThe 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.\n\n```\nfind \"$HOME/Library/Application Support/myapp\" -maxdepth 1 -type d -name 'test-*' -exec rm -rf -- {} +\n```\n\nThere 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.\n\nIn [codex#42355](https://github.com/openai/codex/issues/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.\n\nBuilding 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/`.\n\n``` bash\n$ git clean -ndX -- config/skills/old\nWould remove config/\n```\n\nAlways 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`.\n\nIn a [post](https://www.reddit.com/r/ClaudeAI/comments/1xsgltl/) 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.\n\nI 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.\n\n```\ncmd /c dir /AL /S \"D:\\project-wt-old\"       # list junctions and symlinks\ncmd /c rmdir \"D:\\project-wt-old\\models\"     # remove only the link, no /s\ngit worktree remove \"D:\\project-wt-old\"     # without --force\n```\n\nA 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.\n\n[#107274](https://github.com/anthropics/claude-code/issues/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.\n\nTo 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.\n\nIn [#109627](https://github.com/anthropics/claude-code/issues/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:\n\n```\npkill -f \"gcloud.py compute\" -u <user> --signal 0\n```\n\n`--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.\n\nTesting 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.\n\nThe commands differed, but a few things came up in several cases.\n\nFirst, 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.\n\nSecond, 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.\n\nThird, 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.\n\nFourth, 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.\n\nCollecting 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.\n\nMaking 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](https://code.claude.com/docs/en/checkpointing), do not revert files changed only by Bash commands.\n\nA sandbox that limits writes to the working folder reduces the blast radius no matter where a computation points. Claude Code's [sandbox](https://code.claude.com/docs/en/sandboxing) 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.\n\nWhatever 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).\n\nClaude 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](https://github.com/Dicklesworthstone/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.\n\nHooks 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.\n\nI maintain one such pre-execution hook, [claude-bulkhead](https://github.com/dooly6276/claude-bulkhead), and several of the cases in this post were the starting point for its rules.", "url": "https://wpnews.pro/news/ten-coding-agent-deletions-traced-to-the-shell-command-that-ran", "canonical_source": "https://dev.to/dooly6276/ten-coding-agent-deletions-traced-to-the-shell-command-that-ran-25jf", "published_at": "2026-11-14 18:06:45+00:00", "updated_at": "2026-11-14 18:21:00.019104+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "ai-safety", "developer-tools"], "entities": ["Claude Code", "Codex CLI", "Antigravity", "opencode", "Anthropic", "OpenAI", "Google", "GitHub"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/ten-coding-agent-deletions-traced-to-the-shell-command-that-ran", "markdown": "https://wpnews.pro/news/ten-coding-agent-deletions-traced-to-the-shell-command-that-ran.md", "text": "https://wpnews.pro/news/ten-coding-agent-deletions-traced-to-the-shell-command-that-ran.txt", "jsonld": "https://wpnews.pro/news/ten-coding-agent-deletions-traced-to-the-shell-command-that-ran.jsonld"}}