fix(worktree): exclude .claude/worktrees/ from being seeded into new tasks - #266
fix(worktree): exclude .claude/worktrees/ from being seeded into new tasks#266snehaj wants to merge 1 commit into
Conversation
…tasks CLAUDE_DIR_EXCLUDE already exists specifically to keep per-worktree-local state out of new tasks (plans/, steps.json), but it was missing 'worktrees' -- Claude Code's own runtime worktree registry. Since .claude/worktrees/ in the main repo can itself contain other currently-active worktrees (each a full independent checkout of the project), seeding it into every new task duplicates all of them, every time. In a repo with a handful of active Claude Code worktrees this added ~1.4-1.9GB of pure duplication per new task -- verified live across multiple task creations in a real project -- making task creation and later worktree removal both noticeably slow. Confirmed via 'git worktree list' that anything seeded this way is never a real registered worktree of the target repo; it's always orphaned duplicate content from the copy. Adds test coverage for ensureClaudeSandboxFiles (previously untested): worktrees/ is excluded, plans/ and steps.json remain excluded, and ordinary entries (e.g. skills/) are still seeded correctly.
johannesjo
left a comment
There was a problem hiding this comment.
Review — looks good to merge ✅
Reviewed at b026828. Correct, minimal, and it lands at the one right choke point: ensureClaudeSandboxFiles is the only place .claude/ is ever copied, and .claude is already a reserved symlink name (git.ts:191), so there is no second path that needs the same guard.
What I checked
| Check | Result |
|---|---|
electron/ipc/git-worktree.test.ts on PR head |
38/38 pass |
| Does the new test actually catch the bug? | Reverted git.ts to base → exactly the worktrees test fails, rest pass |
tsc --noEmit |
clean |
eslint on both changed files |
clean |
Merge into main (36 commits ahead of base) |
no conflicts |
| Premise is real | .claude/worktrees/<id> exists locally as a full checkout |
1. The description undersells this — it is a data-safety fix, not just a disk fix
.claude/worktrees/<id>/.git is not a directory, it is a gitlink file:
gitdir: /path/to/repo/.git/worktrees/agent-x
cpSync(..., { dereference: true }) only dereferences symlinks, so that file is copied byte-for-byte. The seeded copy therefore isn't inert duplicate content — it stays wired to the original worktree's admin directory. I reproduced the pre-fix seeding loop against a repo with a real nested worktree:
$ git -C <copy> rev-parse --git-dir
/tmp/…/src/.git/worktrees/agent-x # ← the ORIGINAL repo's admin dir
$ echo written-by-copy > <copy>/poison.txt
$ git -C <copy> add poison.txt # staged from inside the copy
add OK
$ git -C <real-worktree> status --short
AD poison.txt # ← the REAL worktree's index, poisonedSo a git add run inside a seeded copy — by an agent exploring the tree, or any tooling that walks directories — writes into a live, unrelated worktree's index. AD there means "staged for addition, absent from the working tree": a later commit in the real worktree would include a file that does not exist in it.
This also sharpens one line in the description:
Confirmed via
git worktree listthat anything seeded this way is never a real registered worktree of the target repo; it's always orphaned duplicate content from the copy.
The first half is right — I confirmed the copy does not appear in git worktree list. But "orphaned duplicate content" reads as inert, and it isn't: unregistered ≠ disconnected. Suggested rewording for the commit body:
The seeded copy is never itself a registered worktree, but it carries the source worktree's
.gitgitlink — so git commands run inside the copy operate on the original worktree's index and HEAD.
Worth putting in the commit message; it's a stronger argument for this change than the byte count.
2. No backfill for already-affected worktrees (non-blocking, probably correct as-is)
ensureClaudeSandboxFiles also runs on every agent spawn (pty.ts:572), but that path only unlinks symlinks (if (!entry.isSymbolicLink()) continue;) and skips seeding when the destination exists. So worktrees created before this fix keep their duplicated .claude/worktrees/ copy — and its live gitlink — indefinitely.
I think not auto-deleting is the right call: a user who ran /worktree from inside a task worktree would have a genuinely registered nested worktree there, and a blind rmSync would destroy uncommitted work. But given finding #1, existing copies are a latent hazard and not just wasted bytes, so it deserves a line in the PR description or release notes telling users to remove stale .claude/worktrees/ from existing tasks by hand.
3. Minor notes
- Two of the three tests (
plans/steps.json) pass with and without the change — they're regression guards, which is fine and labelled as such. Only theworktreestest is the one that fails on base. No action needed. makeWorktreeDir()creates the fake worktree inos.tmpdir(), outside the repo, sorepoRootis always passed explicitly and therepoRoot === undefined→detectRepoRootbranch stays uncovered. Not worth changing here.
One thing I could not verify
The 1.4–1.9 GB figure. On my checkout the nested worktree is 54 MB because its node_modules is empty. The mechanism is sound — dereference: true resolves the per-entry node_modules symlinks into full copies — so the number is plausible for a repo with installed deps, but I'm taking it on trust rather than having reproduced it.
CLAUDE_DIR_EXCLUDE already exists specifically to keep per-worktree-local
state out of new tasks (
plans/,steps.json), but it was missingworktrees— Claude Code's own runtime worktree registry.Since
.claude/worktrees/in the main repo can itself contain othercurrently-active worktrees (each a full independent checkout of the
project), seeding it into every new task duplicates all of them, every
time. In a repo with a handful of active Claude Code worktrees this
added ~1.4-1.9GB of pure duplication per new task — verified live
across multiple task creations in a real project — making task
creation and later worktree removal both noticeably slow.
Confirmed via
git worktree listthat anything seeded this way isnever a real registered worktree of the target repo; it's always
orphaned duplicate content from the copy.
Adds test coverage for
ensureClaudeSandboxFiles(previously untested):worktrees/is excluded,plans/andsteps.jsonremain excluded, andordinary entries (e.g.
skills/) are still seeded correctly.