Worktree sessions reuse an existing worktree directory from a previous session instead of creating a fresh one

Status Open
Reported on v2.1.197
Maintainer reply None cached
Activity 15 comments · opened Jul 20, 2026

Environment

  • Claude Code 2.1.197 (desktop app session with worktree isolation)
  • macOS (Darwin 24.6.0)

What happened

Starting a new session (new task, worktree isolation enabled) placed the session inside an existing worktree directory that a previous session had created for an unrelated task, instead of creating a fresh worktree. A new branch was created for the new session, but it was checked out into the old worktree directory.

The reflog of the reused worktree shows the history clearly (paths/branches lightly sanitised):

072bd99 HEAD@{2026-07-20 09:14}: checkout: moving from <detached> to claude/<new-task-branch>     <- new session reuses dir
cff5b74 HEAD@{2026-07-18 14:19}: checkout: moving from claude/<old-task-branch> to HEAD
cff5b74 HEAD@{2026-07-17 10:02}: checkout: moving from <detached> to claude/<old-task-branch>
cff5b74 HEAD@{2026-07-15 13:58}: commit: (previous task's commits)

So .claude/worktrees/<old-task-name>-<hash>/ (directory named after the previous task) is now hosting a branch for a completely different task.

Expected

Each new session/task with worktree isolation should get a fresh worktree directory named after its own task/branch, created off the default branch.

Impact

  • The worktree directory name no longer matches the branch/task, which is confusing when several worktrees exist.
  • Leftover untracked/ignored files from the previous task can silently leak into the new task's context or PR.
  • This has happened repeatedly across sessions in the same repo (the user has had to add a standing instruction telling the model never to reuse existing worktrees, but the reuse happens at session setup, before the model can influence it).

Possibly related observation

In the same session, background subagents (Agent tool) appeared to default their working directory to the main checkout rather than the session's worktree — two subagents wrote files to <repo>/docs/... instead of <repo>/.claude/worktrees/<worktree>/docs/... and the files had to be moved manually. If subagent cwd inheritance is a separate issue I can file it separately.

View original on GitHub ↗

7 Comments

xg-gh-25 · 1 month ago

The worktree collision is happening because the session initialization is checking for an existing worktree directory by name but not verifying whether it's stale from a previous session. Git's worktree list will show the directory as "valid" even if the original session that created it is long gone.

Two failure modes here:

  1. State pollution: The reused worktree contains uncommitted changes or a detached HEAD from the previous session, causing unexpected behavior
  2. Lock contention: If the original session is somehow still alive (or never cleaned up its lock), you get a conflict

Root cause check:
Look at how the worktree directory name is generated. If it's deterministic (e.g., based on repo name only), collisions are inevitable. If it includes a session ID, the cleanup logic isn't working.

Fix suggestions:

  1. Verify worktree ownership: Before reusing a worktree, check if it's registered in git worktree list for the parent repo. If not, it's an orphan and should be deleted.
  1. Add session-scoped cleanup: On session start, prune any worktrees older than N minutes that match the expected naming pattern:

``bash
git worktree prune
find /tmp/worktrees -type d -mmin +60 -name "claude-session-*" -exec git worktree remove --force {} \;
``

  1. Use unique directory names: Include a timestamp or UUID in the worktree directory name to guarantee uniqueness per session.

This is tracked in the memory + session management topic — worktree cleanup is part of the broader ephemeral state problem.

---
Context: tracking memory + session management across agent runtimes. SwarmAI. Discussion: T-MEM

xni06 · 1 month ago

Confirming on macOS (26.5.2, Claude Code 2.1.212, Desktop app) — hit this repeatedly this week, including once where the adopted folder belonged to a still-live session. We ran controlled tests today and can add the trigger, the mechanism behind your reflog, and a detection heuristic.

The trigger is archiving: an archived session's folder returns to a reuse pool, deterministically.

  1. New session with worktree enabled → gets folder .claude/worktrees/X-79ed71 on branch claude/X-79ed71 (names match — this is what fresh looks like).
  2. Commit on that branch (unpushed), then archive the session.
  3. Create another worktree-enabled session → it mints a fresh branch claude/Y-d6f752 but checks it out into the old folder X-79ed71. Folder birth time predates the session; folder name no longer matches the branch. The old branch survives unattached with its commit; any uncommitted/ignored files are inherited by the new session.

Archive detaches HEAD but never removes the folder — detached folders are the pool. Watching git worktree list while archiving a session: the folder flips from [claude/<branch>] to (detached HEAD) and stays on disk. That's your reflog line moving from claude/<old-task-branch> to HEAD; the later moving from <detached> to claude/<new-task-branch> is the reuse. Notably, archiving a session whose branch had been renamed off claude/* did not detach it — ownership appears keyed to the minted claude/* branch name. Consistent with that, the session registry can disagree with reality: one recycled session's registry entry showed its minted branch claude/hello-<hash> while its actual cwd was a pre-existing folder from a different task.

Live sessions are affected too: a live session's folder whose branch had been renamed to a team convention (feat/<ticket>-…) was adopted by a brand-new session while the first was still open — its checkout switched out from under it.

Detection heuristic / workaround (we run this as a session-start gate now): a fresh worktree has folder basename == branch minus claude/, a just-now birth time, and clean status. Any of these failing means a recycled folder:

basename "$(git rev-parse --show-toplevel)"        # folder name
git branch --show-current                          # branch name
stat -f "%SB" "$(git rev-parse --git-dir)/gitdir"  # folder BIRTH time (mtime lies)
git status --porcelain                             # inherited leftovers

Expected: a worktree-enabled new session always creates a new folder, and archive either removes the worktree (git worktree remove + prune) or permanently retires it — never feeds it to the next session, uncommitted files and all.

kcarriedo · 1 month ago

The worktree-directory reuse bug you are hitting is a symptom of the session harness using a content-addressed or fixed-pattern directory name (typically derived from the worktree branch prefix) rather than a session-unique identifier. When the old branch was cleaned up but the worktree directory was not pruned, the next session sees the directory as available and checks into it.

Two observations from running long-lived parallel agent fleets that might be useful context for the Anthropic team:

  1. Directory name and branch name need to be co-derived. If the directory name is stable (based on repo + task name pattern) and the branch inside it changes, you get exactly the reflog history you showed: the old task's commits are still in that directory's history, and the new task's branch appears on top of them. The fix is either to make the directory name session-unique (UUID suffix), or to refuse to reuse a directory whose branch history does not belong to this session.
  1. A "worktree registry" approach helps. Before creating a new worktree, the harness should check a session-state file listing all active worktrees (path + owning session ID + last-active timestamp). If a path is in the registry and the owning session is no longer live, it gets pruned first. If a path is in the registry and the session is still live, a new path is allocated. This is the same class of problem as PID file management.

In the meantime: git worktree prune before starting a new session clears the stale entries, though it does not prevent the incorrect allocation if the directory still exists on disk.

kieranbenton · 1 month ago

Confirming this independently, with a slightly different angle: direct evidence from the app's own local state, not just git reflog.

Desktop keeps a worktree lease registry at ~/Library/Application Support/Claude/git-worktrees.json. On my machine it currently shows entries like:

"inventory-sync-cpu-spike-1e5b7c": {
  "path": ".../ticketing.worktrees/ticketing/inventory-sync-cpu-spike-1e5b7c",
  "leasedBy": "local_a84e5933-220d-47a6-a43e-1df64a7b669a",
  "branch": "claude/cleanup-branches-worktrees-7ae542",
  ...
}

The directory name (inventory-sync-cpu-spike-1e5b7c) is from a completely unrelated earlier task; the branch field shows it's currently leased to a different session on a different branch entirely. The leasedBy field name itself confirms @xni06's theory in the comment above — this is a deliberate lease pool, not an accidental collision. Archiving a session appears to release the lease (making the slot available for reuse) rather than actually removing the git worktree, which is presumably why cleanup settings (ccAutoArchiveOnPrClose, confirmed enabled in my claude_desktop_config.json) don't prevent this from recurring.

Worth noting there's no documented setting to opt out of this pooling — checked the settings reference and worktrees docs and found nothing under a worktree.* namespace for it. For anyone relying on the worktree folder name to identify which task is which (e.g. picking the right one to open in an IDE), this makes that unreliable once a slot has been recycled even once.

AngryFinn · 1 month ago

I am experiencing this issue as well on Claude for Windows 1.24012.9 (03c61d)

kcarriedo · 1 month ago

The lease registry finding from @kieranbenton (~/Library/Application Support/Claude/git-worktrees.json) is the missing piece that explains why git worktree prune does not help here: the app is managing a pool at the application layer, separate from git's own worktree tracking. Git sees valid worktrees (or prunable ones), but the app's pool decision happens before git is consulted.

This matters for the fix design. The corruption is not a git bookkeeping error - it is an application-level pool allocator that releases a slot (on archive) but does not clean the directory, then hands that dirty slot to the next session. The fix belongs in the pool allocator: release should include a git worktree remove, not just a lease entry update.

A few observations from running long-lived parallel agent fleets that may be useful context for the Anthropic team:

The leasedBy field in the registry suggests the app already has the concept of "a session owns this worktree" - the gap is that archive clears the owner without removing the directory, creating the pool-of-dirty-slots problem. If archive ran git worktree remove --force on the slot before clearing leasedBy, the pool would only contain genuinely clean entries.

The case @xni06 documented - a live session's folder being adopted while it was still open - is the harder variant. That happens when the branch inside the worktree was renamed off the claude/* prefix, which apparently releases the lease even though the session is active. The fix there is to key lease ownership on the session ID (from leasedBy), not on the branch name pattern.

For anyone running multiple parallel agent sessions right now and hitting this: the detection script @xni06 posted works reliably. We use a similar check as a pre-task gate: compare folder basename to branch suffix, verify birth time is within the session window, and run git status --porcelain to catch inherited untracked files before the agent sees them.

xni06 · 1 month ago

@kcarriedo nailed it — the pool decision happens at the application layer, above git, and that fact generalizes further than it might seem. Any mitigation working below the app (pruning, cleaning, renaming branches) can just get overridden by the allocator on its next pass. That's why git worktree prune never actually helps here. So we gave up on detecting/cleaning pooled slots and just stopped using them entirely.

Our workaround: create the worktree yourself, outside the repo, with the harness checkbox unticked.

Start the session with the worktree box unticked, so the app never allocates a slot for you. Then, from the main checkout:

git fetch origin main --quiet
git worktree add ~/dev/worktrees/<repo>/<TICKET> -b <branch> origin/main

...and anchor the session to that path using the native EnterWorktree tool.

Why this actually works: the pool only tracks folders the app itself created and logged in git-worktrees.json. If you create the folder yourself, it never shows up there — no lease to release, nothing for the allocator to reuse. Easy to check on your own machine: compare git worktree list against the registry's keys. Self-created worktrees never appear in the registry, every .claude/worktrees/ folder does, and anything sitting at leasedBy: null is basically waiting in line to be reused.

We'd tried a detection gate before landing on this approach. It works, but only catches the problem after you've already been handed a dirty slot — which is strictly worse than avoiding the handoff in the first place.

A few nice side effects fall out of just owning the path yourself: the folder name stays as your ticket key permanently (which also solves @kieranbenton's issue about folder names becoming meaningless over time), the branch gets its real name right from creation, and being outside the repo altogether sidesteps the nested-.claude/ discovery issues tracked separately in #48967 and #69026.

To be upfront about the trade-offs:

  • Teardown is fully manual — git worktree remove + git branch -d. Not much of a downside though, since archive doesn't reclaim harness-created worktrees either.
  • The app's session list will show these sessions at the repo root instead of the actual worktree, so you lose that UI attribution — the branch name and folder name become your source of truth instead.
  • One gotcha if you're anchoring via EnterWorktree: /clear resets the session's cwd back to its launch directory and silently drops the anchor. After a clear, you're back at the main checkout while the worktree still sits on disk — so anything that writes files needs to re-anchor first, or your edits land on the default branch instead. Easy to work around once you know about it, easy to get burned by if you don't.

To be clear, none of this actually fixes anything. The underlying allocator behavior still needs to be addressed, and the real bug is that archive releases a lease without removing the directory. This is just a way to sidestep the problem today, without needing a setting that doesn't currently exist.

Showing cached comments. Read the full discussion on GitHub ↗