Windows: stale-worktree cleanup rm -rf traverses NTFS junctions and deletes data OUTSIDE the worktree (~800 GB data loss)
Summary
Claude Code's stale-worktree cleanup deletes worktrees with a recursive rm -rf (Git-for-Windows/MSYS rm.exe). On Windows, that rm does not treat NTFS junctions as links — it descends into them and deletes the target's contents. A worktree containing junctions that point outside itself (in our case: junctions from <worktree>/logs/... into the main repo's gitignored data directories) therefore becomes a landmine: when cleanup removes the worktree, it silently deletes data outside the worktree.
In our case this deleted ~800 GB of collected market data (tick logs, on-chain trade caches, kline caches) whose upstream source retains only ~7 days — most of it permanently unrecoverable. No agent issued any delete command; the deletion is harness housekeeping, so it appears in no session transcript, produced no permission prompt, and bypasses the Recycle Bin.
Environment
- Claude Code 2.1.123, Windows 11 Pro (build 26200)
- git version 2.49.0.windows.1 (its bundled MSYS
rm.exeis what executed the deletions — confirmed via Windows Prefetch) - Worktrees under
<repo>/.claude/worktrees/<name>created by earlier sessions; several hadlogs/(or subdirs of it) junctioned to the main repo (cmd /c mklink /J ...) so worktree sessions could read large gitignored datasets without copying
What happened (timeline, condensed)
- Several worktree sessions went dormant for weeks. During that time, other sessions installed NTFS junctions inside those worktrees (
<worktree>/logs/<subdir>→<main-repo>/logs/<subdir>). - The user reopened the app and revived two of the dormant chats after weeks away. Within ±15 s of the first revival, stale-worktree cleanup ran: four old worktrees present the previous evening were removed (a second batch aligned with the first app activity of the morning).
- The cleanup's
rm -rfwalked through the junctions and deleted the main repo's data behind them. Victim set = exactly the union of the deleted worktrees' junction layouts; siblings that no worktree had junctioned survived.
Evidence
- Windows Prefetch:
RM.EXE(MSYS) executed during the deletion window, preceded byDF.EXE; no interactive shell history (PSReadLine and.bash_historyempty of any such command) — consistent with non-interactive harness execution. - NTFS USN journal: 12k+
File delete | Closerecords for the affected files in the window. - Exhaustive scan of every session/subagent transcript on the machine: no tool call contains the deletion — it was not an agent command.
git worktree list+ directory snapshots: 4 worktrees present on day N evening, gone day N+1 morning; surviving worktrees still carried live junctions into the main repo's data dirs (since manually removed).
Suggested fix
When deleting a worktree (or any harness-owned directory) on Windows:
- Pre-scan for reparse points (
FILE_ATTRIBUTE_REPARSE_POINT) and remove them as links first (rmdiron a junction removes only the link — never the target), then recursively delete the remainder; or - use a deletion routine that refuses to traverse reparse points; and
- ideally log each worktree removal (path + trigger) somewhere durable — this incident took two days to attribute precisely because the cleanup leaves no record.
A defensive extra: if a worktree contains reparse points pointing outside itself, surface a warning / require confirmation instead of cleaning silently.
Repro sketch (safe)
# 1. scratch "precious" data OUTSIDE a repo
mkdir C:\tmp\precious; echo x > C:\tmp\precious\keep.txt
# 2. simulate a stale worktree with a junction into it
mkdir C:\tmp\wt; cmd /c mklink /J C:\tmp\wt\data C:\tmp\precious
# 3. what the cleanup effectively runs (Git-for-Windows rm):
& 'C:\Program Files\Git\usr\bin\rm.exe' -rf C:\tmp\wt
# 4. C:\tmp\precious\keep.txt is gone on affected rm builds
Happy to provide any further forensic detail (timestamps, USN excerpts) privately if useful.
Showing cached comments. Read the full discussion on GitHub ↗
3 Comments
Additional forensic detail, as offered. All times 2026-07-07; machine local = BST (UTC+1).
Precise timeline
| Time (local) | Event |
|---|---|
| Jul 6 ~18:50 | Directory listing shows 11 named worktrees under
.claude/worktrees/(4 of them later deleted:mystifying-albattani-61e7ab,adoring-kowalevski-3b69f5,dreamy-johnson-4b868a,jovial-rhodes-ad0085) || 04:59–05:19 | Window 1: all 9 of our rclone pull loops (5-min cadence, separate console tabs) go silent simultaneously, then resume staggered — coincides with the user's first terminal/app activity of the day |
| 09:59:19–09:59:39 | Window 2 starts: pull loops go silent again |
| 09:59:24 | User revives a weeks-dormant worktree session (its transcript shows five
"No completion record was found for this background shell command from the previous..."task-notifications delivered at revival — orphaned background tasks from its June 11 life) || 10:00:00–10:00:13 | Three OTHER dormant worktree sessions' transcript files are touched (mtime), sequentially — same maintenance sweep |
| 09:21→10:5x | USN journal window: 12,108
File delete | Closerecords for the affected trees (see below) — bulk (10,020) in the 10:00 hour || 10:20:26 / 10:23:59 / 10:24:59 | Prefetch last-run stamps:
DF.EXE,RM.EXE,TR.EXE(MSYS binaries from Git for Windows 2.49.0) || Jul 7 midday |
git worktree list+ directory diff: the 4 worktrees above are gone |Window 1's
rmevidence was overwritten in Prefetch (it keeps only each executable's last run — the 10:23 run restamped the same.pf), and the USN journal (32 MB rolling) had rotated past it; the second window is fully documented.USN journal excerpts
fsutil usn readjournal D: csv(elevated), filtered to the affected file patterns — 262,022 records scanned, 12,108 delete-matches. Sample rows:Hourly distribution of the 12,108: 2,076 in 09:xx · 10,020 in 10:xx · 12 in 11:xx. The journal caught only the tail — total deletion was several hundred thousand files (~800 GB freed between Jul 6 evening and Jul 7 midday).
Prefetch (window 2)
DF.EXE(disk-free) immediately precedingRM.EXEis consistent with cleanup tooling checking space around removal.Junction layouts (from surviving sibling worktrees, since removed manually)
Two patterns existed, both installed by other sessions weeks after the victim worktrees' own sessions went dormant (
cmd /c mklink /Jcalls are present in those other sessions' transcripts; the deleted worktrees' own transcripts contain no junction creation — provisioning happened outside recorded tool calls):<worktree>\logs→<main>\logs(3 surviving examples)<worktree>\logs\{binance,cache,polymarket,ticks,ticks_lightsail}(5 junctions) and another with 7 subdir junctions incl.shadow,trades,user_analysisThe union of the deleted worktrees' junction sets matches the victim tree list exactly (subtrees never junctioned anywhere survived untouched).
Negative evidence (why we're confident it's harness housekeeping)
find -delete/xargs rm/ split-path forms): no agent command deletes these trees..bash_historyentries (non-interactive execution).winfrrecovered 0 files.Happy to pull anything else that would help (specific USN ranges, the full prefetch dump, worktree transcript excerpts).
Going through the report, I kept coming back to the same point. Nobody verified what the command was actually going to do before executing it.
Once it ran, recovery wasn't really the problem anymore. The opportunity to prevent it had already passed.
Full disclosure, I've been building a pre-execution hook for this exact problem. It wouldn't promise recovery here. It would simply stop the command and show the real blast radius before anything happens. Happy to share it if anyone is interested.
Sorry about the loss 🙏🏻
Fixed in 2.1.205 — changelog: "Fixed Windows worktree removal deleting files outside the worktree when an NTFS junction or directory symlink existed inside it" (https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md). Closing — reply to reopen if it still happens on the latest version.
🤖 Generated with Claude Code