Windows: stale-worktree cleanup rm -rf traverses NTFS junctions and deletes data OUTSIDE the worktree (~800 GB data loss)

Status Fixed / completed
Reported on v2.1.123
Maintainer reply ✓ Yes — bcherny
Activity 4 comments · opened Jul 7, 2026 · closed Aug 19, 2026
💡 Likely answer: A maintainer (bcherny, collaborator) responded on this thread — see the highlighted reply below.

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.exe is what executed the deletions — confirmed via Windows Prefetch)
  • Worktrees under <repo>/.claude/worktrees/<name> created by earlier sessions; several had logs/ (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)

  1. 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>).
  2. 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).
  3. The cleanup's rm -rf walked 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 by DF.EXE; no interactive shell history (PSReadLine and .bash_history empty of any such command) — consistent with non-interactive harness execution.
  • NTFS USN journal: 12k+ File delete | Close records 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:

  1. Pre-scan for reparse points (FILE_ATTRIBUTE_REPARSE_POINT) and remove them as links first (rmdir on a junction removes only the link — never the target), then recursively delete the remainder; or
  2. use a deletion routine that refuses to traverse reparse points; and
  3. 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.

View original on GitHub ↗

3 Comments

bedroomlab · 3 months ago

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 | Close records 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 rm evidence 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:

232332735496,"BTC_5m_20260212_0945.jsonl",52,0x80000200,"File delete | Close","07/07/2026 09:21:32",...
232332735608,"BTC_5m_20260212_1000.jsonl",52,0x80000200,"File delete | Close","07/07/2026 09:21:32",...
232332735720,"BTC_5m_20260212_1000.parquet",56,0x80000200,"File delete | Close","07/07/2026 09:21:32",...

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)

09:35:30 TRUSTEDINSTALLER.EXE  09:58:18 ROUTE/NETSH/IPCONFIG   10:00:09 GIT-LFS.EXE (x2)
10:16:39 SMARTSCREEN.EXE       10:16:46 FILECOAUTH.EXE          10:20:26 DF.EXE
10:23:59 RM.EXE                10:24:59 TR.EXE

DF.EXE (disk-free) immediately preceding RM.EXE is 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 /J calls are present in those other sessions' transcripts; the deleted worktrees' own transcripts contain no junction creation — provisioning happened outside recorded tool calls):

  • whole-tree: <worktree>\logs → <main>\logs (3 surviving examples)
  • per-subdir: e.g. <worktree>\logs\{binance,cache,polymarket,ticks,ticks_lightsail} (5 junctions) and another with 7 subdir junctions incl. shadow, trades, user_analysis

The 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)

  • Every session/subagent transcript on the machine swept at the tool-call level (including find -delete / xargs rm / split-path forms): no agent command deletes these trees.
  • No PSReadLine or .bash_history entries (non-interactive execution).
  • No Task Scheduler entries, Run keys, startup scripts, cron stores, or repo code paths capable of it (full deletion-surface audit of our own codebase came up empty).
  • Recycle Bin empty (programmatic deletion), no VSS shadow copies, TRIM'd SSD — winfr recovered 0 files.

Happy to pull anything else that would help (specific USN ranges, the full prefetch dump, worktree transcript excerpts).

pyxt4r · 3 months ago

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 🙏🏻

bcherny collaborator · 1 month ago

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

Showing cached comments. Read the full discussion on GitHub ↗