Skip to content

fix(worktrees): prevent accidental worktree deletion (3 paths)#25

Merged
aletc1 merged 1 commit intodevfrom
claude/great-khorana-e96fc5
Apr 18, 2026
Merged

fix(worktrees): prevent accidental worktree deletion (3 paths)#25
aletc1 merged 1 commit intodevfrom
claude/great-khorana-e96fc5

Conversation

@aletc1
Copy link
Copy Markdown
Owner

@aletc1 aletc1 commented Apr 18, 2026

Summary

Audit of every removeWorktree / fs.rm call site found three bugs that could destroy worktree source code — including uncommitted work — without explicit user consent. After this change, the only path that deletes a worktree is archiving a workspace with the "Delete worktree" checkbox explicitly checked.

Bugs fixed

🔴 Project "Remove" deleted worktrees despite dialog promising it wouldn't

src/renderer/components/dialogs/settings-tabs/agents-project-worktree-tab.tsx shows "Remove from your list. Files on disk will not be deleted." But projects.delete ran removeWorktree() for every chat in the project and rm -rfd the slug directory. One click → every worktree destroyed, with no checkbox, no warning, no uncommitted-changes check.
Fix: projects.delete now only removes the DB row, kills terminals, and aborts Claude sessions. Worktree folders on disk are untouched — honoring the dialog's promise.

🔴 Startup orphan scanner silently deleted worktrees

5 seconds after every app start, scanWorktreeOrphans() walked ~/.21st/worktrees/ and fs.rmd any directory not found in chats.worktreePath. Safety margin: just 60 seconds. Triggerable by DB init failure (only logged, scanner still runs), migration error, dev↔prod switch (different userData paths), userData restore, or a transient SQLite error. No user consent, no quarantine, irreversible.
Fix: The auto-scan setTimeout is removed from src/main/index.ts. scanWorktreeOrphans() is kept in place for a possible future opt-in "Find orphan worktrees…" settings UI.

🟡 chats.delete always deleted worktree without opt-in

The tRPC mutation unconditionally called removeWorktree() whenever chat.branch was set. Not wired to the main sidebar today, but exposed — any future UI, shortcut, or automation would lose worktrees silently. Inconsistent with chats.archive which requires an explicit flag.
Fix: Added deleteWorktree: z.boolean().default(false) to the input schema and gated the removeWorktree() call on it, mirroring the archive flow.

Defensive hardening

removeWorktree() now logs the target path and caller stack before running fs.rm. Cheap forensics for any future unexpected loss.

Not changed (verified safe and kept as-is)

  • chats.archive with "Delete worktree" checkbox — still the single allowed deletion path
  • Batch archive — never passes deleteWorktree, unchanged
  • removeWorktree() path-prefix guard (~/.21st/worktrees/ only)
  • Undo-archive (⌘Z, 10s window)

Files changed

File Change
src/main/lib/trpc/routers/projects.ts Remove worktree + slug-dir deletion from delete
src/main/index.ts Remove startup auto-scan setTimeout
src/main/lib/trpc/routers/chats.ts Add deleteWorktree flag to delete, gate removal on it
src/main/lib/git/worktree.ts Log path + caller before fs.rm

Test plan

  • Project Remove preserves worktrees. Create project with 2 worktree chats, echo unsaved > NEVER_LOSE_ME.txt in each, Settings → Projects → Remove. Verify both worktrees + files still on disk, chats gone from sidebar.
  • Archive + checkbox CHECKED still deletes. Archive a worktree chat with "Delete worktree" checked → worktree folder removed.
  • Archive + checkbox UNCHECKED preserves. Same flow, box unchecked → worktree folder remains.
  • Startup no longer auto-deletes. Create worktree chat, quit, DELETE FROM chats WHERE worktree_path LIKE '%<folder>%' via sqlite, relaunch, wait 90s → directory still exists and log is silent.
  • Forensic log fires. During any deletion the console shows [Worktree] removeWorktree called: path=… caller=….
  • Grep audit: rg "removeWorktree" src/main shows callers only in chats.archive (gated) and chats.delete (now gated).

Out of scope

  • Migrating fs.rm to OS trash (cross-platform work, separate PR)
  • Building the "Find orphan worktrees" settings UI

Audit found three code paths that could destroy worktree source code
without explicit user consent. Worktrees now only get deleted through
the archive flow with the "Delete worktree" checkbox explicitly checked.

- projects.delete no longer removes worktrees or the slug directory.
  The "Remove Project" dialog promises "Your files will not be deleted";
  the backend now honors that promise. Only DB rows + terminals + Claude
  sessions are cleaned up.
- Startup orphan scanner is no longer auto-invoked. Any automatic
  deletion risks destroying uncommitted code if the DB is empty, stale,
  or transiently errors. scanWorktreeOrphans() is kept for a future
  opt-in settings UI.
- chats.delete gains a deleteWorktree flag (default false) and only
  calls removeWorktree when the caller opts in, matching chats.archive.
- removeWorktree now logs path + caller stack before deleting so any
  unexpected loss can be traced.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@aletc1 aletc1 merged commit f2cc80e into dev Apr 18, 2026
@aletc1 aletc1 deleted the claude/great-khorana-e96fc5 branch April 18, 2026 15:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant