fix(worktrees): prevent accidental worktree deletion (3 paths)#25
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Audit of every
removeWorktree/fs.rmcall 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.tsxshows "Remove from your list. Files on disk will not be deleted." Butprojects.deleteranremoveWorktree()for every chat in the project andrm -rfd the slug directory. One click → every worktree destroyed, with no checkbox, no warning, no uncommitted-changes check.Fix:
projects.deletenow 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/andfs.rmd any directory not found inchats.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
setTimeoutis removed fromsrc/main/index.ts.scanWorktreeOrphans()is kept in place for a possible future opt-in "Find orphan worktrees…" settings UI.🟡
chats.deletealways deleted worktree without opt-inThe tRPC mutation unconditionally called
removeWorktree()wheneverchat.branchwas set. Not wired to the main sidebar today, but exposed — any future UI, shortcut, or automation would lose worktrees silently. Inconsistent withchats.archivewhich requires an explicit flag.Fix: Added
deleteWorktree: z.boolean().default(false)to the input schema and gated theremoveWorktree()call on it, mirroring the archive flow.Defensive hardening
removeWorktree()now logs the target path and caller stack before runningfs.rm. Cheap forensics for any future unexpected loss.Not changed (verified safe and kept as-is)
chats.archivewith "Delete worktree" checkbox — still the single allowed deletion pathdeleteWorktree, unchangedremoveWorktree()path-prefix guard (~/.21st/worktrees/only)Files changed
src/main/lib/trpc/routers/projects.tsdeletesrc/main/index.tssetTimeoutsrc/main/lib/trpc/routers/chats.tsdeleteWorktreeflag todelete, gate removal on itsrc/main/lib/git/worktree.tsfs.rmTest plan
echo unsaved > NEVER_LOSE_ME.txtin each, Settings → Projects → Remove. Verify both worktrees + files still on disk, chats gone from sidebar.DELETE FROM chats WHERE worktree_path LIKE '%<folder>%'via sqlite, relaunch, wait 90s → directory still exists and log is silent.[Worktree] removeWorktree called: path=… caller=….rg "removeWorktree" src/mainshows callers only inchats.archive(gated) andchats.delete(now gated).Out of scope
fs.rmto OS trash (cross-platform work, separate PR)