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 18 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 ↗

18 Comments

xg-gh-25 · 2 months 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 · 2 months 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 · 2 months 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 · 2 months 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 · 2 months ago

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

kcarriedo · 2 months 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 · 2 months 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.

keighleymcf · 2 months ago

Corroborating this on Claude Code 2.1.104 / Desktop app 1.24012.9 (macOS) — multiple times a new session was placed inside a .claude/worktrees/<dir> directory that had been created earlier for an unrelated task, rather than a fresh worktree. The reflog confirmed that same directory had hosted several different branches across sessions.

I dug into where this comes from; sharing the mechanism since the thread so far describes the symptom but not the cause. Everything below is reverse-engineered from the shipped app.asar and local logs/registry — not official.

It's a lease pool (WorktreePool), not fresh-per-session. CC Desktop keeps a pool of worktrees per repo and leases them to sessions, returning them to the pool on session end. Registry at ~/Library/Application Support/Claude/git-worktrees.json:

"<worktree-dir>": {
  "path": "~/<repo>/.claude/worktrees/<worktree-dir>",
  "leasedBy": "<session-id>", "branch": "<branch>",
  "sourceBranch": "main", "createdAt": <timestamp>
}

Reuse is explicit in ~/Library/Logs/Claude/main.log:

[WorktreePool] Reused worktree <worktree-dir> for session <session-id> (was leased by none)

The pool should default to OFF but is active — in app.asar, worktreePoolEnabled defaults to false, yet it's running here, so it appears to be enabled by a feature gate shipped in a recent update (first [WorktreePool] activity in my logs began after the auto-update to 1.24012.9). That's why this reads as a regression: a behaviour-changing feature that defaults off was turned on for existing installs, undocumented (the public docs still state "every new session gets its own worktree automatically").

Pruning is by count cap, not age-based. Decompiled sweep logic:

group worktrees by repo → sort by recency (newest first)
if maxWarm > 0:  keep the maxWarm newest, reap the rest regardless of age
else:            keep the newest, reap others idle ≥ reapAfterMs

backed by hidden app preferences ccMaxWarmWorktrees (default 3) and ccWorktreeReapAfterHours (default 24), neither surfaced in any documented settings.json. With maxWarm=3 it keeps the 3 most-recently-used worktrees per repo and reaps the rest on each sweep. In practice this means idle worktrees vanish the next time a sweep runs once they are pushed out of the top 3 — and it's entirely separate from cleanupPeriodDays (30d), which has no effect on it.

Two follow-on behaviours worth noting (also reported, closed unaddressed):

  • The sweep reaps worktrees that still carry a leasedBy session when that session isn't running/pinned (Removing worktree "<worktree-dir>" (leased by <session-id>)), or possibly just when there are 3+ other more recent sessions, so a session you intended to resume can lose its worktree.
  • When a leased worktree is removed, the session silently retargets the main checkout: [CCD] Worktree at ~/…/.claude/worktrees/<worktree-dir> was deleted; falling back to origin repo ~/<repo> — a session can end up operating directly on main's working copy with no notice.

Possibly related: #43730, #62431, #46444, #40968, #48405, #34344, #49986; opt-out requests #31896, #50109.

Requests: (1) create a fresh worktree per session as documented, or document the pool/lease model and expose an opt-out (worktreePoolEnabled); (2) surface ccMaxWarmWorktrees / ccWorktreeReapAfterHours as documented settings and align retention with cleanupPeriodDays; (3) never silently fall a session back to the origin repo when its worktree is removed, at least actively notify the user of the change.

sun-13 · 2 months ago

Another data point from the desktop app, plus a case where reuse actively breaks the archive cleanup

Same behavior here (desktop app on macOS, Aug 2026). Some concrete numbers from one repo's worktree list:

  • One pooled directory (optimistic-cannon) has now hosted 3 different sessions on 3 different branches; another (jovial-joliot-*) hosts a branch whose name has nothing to do with either the directory or the session title. With ~10 worktrees on disk, git worktree list answers neither "which session is this?" nor "can I delete this?"
  • After archiving a session whose PR merged, the pool keeps the directory (reset + checkout --detach) for reuse instead of removing it. From the outside this is indistinguishable from "archive cleanup failed" — I only understood what happened after digging through the reflog.

Reuse also defeats the cleanup safety checks. Sessions leave untracked files behind (in my case .claude/launch.json, written when a dev-server preview is started). In a fresh-per-session model that file would die with the worktree. With reuse, it persists and marks the worktree permanently dirty — which then blocks the very safety check the archive cleanup relies on. 6 of my 10 lingering worktrees are "dirty" solely because of that one leftover file.

The cost/benefit has shifted for pnpm repos. As I understand it, the pool exists to amortize checkout + node_modules warm-up. With pnpm's content-addressable store, installing into a fresh worktree is a fast hardlink operation — we switched our repos to pnpm partly for this reason. For such repos the pool saves very little, and pays for it with the 1 session = 1 worktree = 1 branch mental model.

Request: an opt-out ("always create a fresh worktree per session"), or failing that, rename the directory to match the new session/branch on reuse and clean untracked leftovers before re-lease.

kcarriedo · 2 months ago

Same pattern here -- new worktree session inherits the directory of a previous one, even when the branch is fresh. The file contamination issue (leftover ignored files carrying over) is the real footgun because it's silent: the agent doesn't know it's working in a dirty context.

The subagent defaulting to the main checkout is a separate but related problem. If you're running parallel sessions with worktrees and one subagent writes to main instead of its worktree, you get cross-session writes with no error.

Workaround that's helped in the meantime: explicitly set the cwd in the agent invocation and add a hook step that verifies git worktree list matches the expected path before any file writes. Verbose, but at least it fails loudly.

Worth flagging for anyone running parallel agent sessions: until this is fixed, validate your working directory explicitly at session start.

stefanguha · 2 months ago

Same behaviour on Windows, with the same lease pool

This is labelled platform:macos and every comment so far is macOS. It reproduces on Windows 11, desktop app, Claude Code 2.1.222, with the pool @kieranbenton found — on Windows the lease file lives at %APPDATA%\Claude\git-worktrees.json.

The entry for one worktree there right now (topics redacted):

"<topic-a>-<hash>": {
"name": "<topic-a>-<hash>",
"path": "...\\.claude\\worktrees\\<topic-a>-<hash>",
"branch": "claude/<topic-e>-<hash>",
"sourceBranch": "main",
"leasedBy": "local_<uuid>"
}

name is the first session's topic. branch is the fifth session's. Same directory.

Across the whole file: 45 entries, 25 currently leased, 16 sitting in the pool with pooledAt set and leasedBy: null — released, but not removed.

Severity: five sessions in one directory

Reflog of that worktree over four days (topics redacted):

2026-08-13 02:06 checkout: moving from <sha> to claude/<topic-e>-<hash>
2026-08-13 00:01 checkout: moving from <sha> to claude/<topic-d>-<hash>
2026-08-11 13:42 checkout: moving from <sha> to claude/<topic-c>-<hash>
2026-08-11 12:17 checkout: moving from <sha> to claude/<topic-b>-<hash>
2026-08-09 23:02 (worktree created for claude/<topic-a>-<hash>)

Five unrelated sessions, five branches, one directory that has described only the first of them since 2026-08-09.

Confirming @kcarriedo's Jul 28 point from the Windows side

Archiving a session does release the slot without removing the worktree — the 16 released-but-present entries above are what that leaves behind. So archiving is already the lifecycle signal the pool listens to; it just clears leasedBy instead of running git worktree remove first.

Closing that gap would give a strict 1:1 session/worktree relationship with an explicit, user-initiated cleanup trigger, and needs no new UI: archiving is a deliberate act performed once, at the moment the user actually knows the work is finished. The current exit-time keep/remove prompt asks at the wrong moment — a session is exited and resumed many times before it is done, so the honest answer is almost always "keep", which is exactly what fills the pool.

One consequence not yet raised in this thread

Because name is frozen at creation and branch is not, the directory shows an unrelated earlier session's topic for the entire life of every later session that leases it — in the terminal path, the editor title bar, and any screen share. Where sessions cover personal or confidential subjects that is a confidentiality problem, not only a cosmetic one. I have filed that half separately as #86239, since the fix there is a naming option rather than a lifecycle change.

Kagami · 1 month ago

Basically worktree feature is broken in Claude Code right now.

I had 4 chats created with worktree checkbox each: so 4 worktrees with 4 branches.
I created 5th chat again with worktree checkbox and it re-used one of my 4 worktree directories and switched it to a new branch.

So I can't use the worktree feature in the Claude Code anymore.

Kagami · 1 month ago

I think I found one case when it would do it (Claude App for macOS 1.37937.3)

I still have 4 chats with 4 worktrees, each checked out at their own branches.
So 4 chats + 4 worktrees + 4 branches.

Yesterday it was all fine. Today I opened terminal, cd into the repo and typed git worktree list.
And suddenly 2 worktrees were shown in "detached HEAD" state.
I opened ~/Library/Application Support/Claude/git-worktrees.json and those 2 same worktrees were marked as "leasedBy": null.

Then I clicked every one of the 4 chats and suddenly worktrees switched back to the correct branches and leasedBy isn't empty anymore.

So I guess it's some sort of auto cleanup feature (at least in one case) which detaches & re-uses worktrees for "inactive" chats.
Which is completely weird behavior because I'm still planning to work on every one of my 4 chats. I don't want their corresponding worktree to be re-used by the new chat.

rasmushn · 1 month ago

FWIW I just saw the "auto-archive" setting under Settings in the Claude Desktop app on Windows.

Kagami · 1 month ago

I have set

  • Archive inactive sessions = Never
  • Auto-archive after PR merge or close = Disabled

so it could be some other auto cleanup or some bug.

Maybe this one:

Classify session states Allow Claude to automatically classify sessions as blocked, ready for review, or done. Classifying sessions counts towards your plan usage. Applies to new sessions.

Will test.

sugilog · 1 month ago

+1

millardjake · 6 days ago

Are folks finding that this got fixed? I noticed in the recent desktop app changelogs some notes about worktree fixes

AngryFinn · 6 days ago
Are folks finding that this got fixed? I noticed in the recent desktop app changelogs some notes about worktree fixes

On Windows, and it's still reusing old worktrees. It does not look like anything has been fixed.