Skip to content

fix(worktree): exclude .claude/worktrees/ from being seeded into new tasks - #266

Open
snehaj wants to merge 1 commit into
johannesjo:mainfrom
snehaj:fix/dont-seed-claude-worktrees-into-new-tasks
Open

fix(worktree): exclude .claude/worktrees/ from being seeded into new tasks#266
snehaj wants to merge 1 commit into
johannesjo:mainfrom
snehaj:fix/dont-seed-claude-worktrees-into-new-tasks

Conversation

@snehaj

@snehaj snehaj commented Sep 1, 2026

Copy link
Copy Markdown

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.

…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 johannesjo left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, poisoned

So 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 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.

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 .git gitlink — 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 the worktrees test is the one that fails on base. No action needed.
  • makeWorktreeDir() creates the fake worktree in os.tmpdir(), outside the repo, so repoRoot is always passed explicitly and the repoRoot === undefineddetectRepoRoot branch 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.

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.

2 participants