[BUG] ExitWorktree (remove) reports success but orphans the worktree (dir + admin entry + branch); intermittently corrupts parent repo core.bare
Environment
- Claude Code 2.1.177 (CLI)
- macOS 26.5.1 (Darwin 25.5.0), git 2.50.1 (Apple Git-155)
worktree.baseRef: "fresh". AWorktreeCreatehook relocates worktrees out of.claude/worktrees/to~/code/.worktrees/<repo>/<name>/, so the session cwd becomes that path. No branch renames involved.
Summary
Two related failures in ExitWorktree action="remove" when the session's working directory is inside the worktree being removed:
- (100% reproducible) Orphaned worktree despite a success message. The first
removeis refused (Could not verify worktree state … Refusing to remove without explicit confirmation); after re-invoking withdiscard_changes: trueit returnsExited and removed worktree at …, but the worktree directory, its.git/worktrees/<name>/admin entry, and theworktree-<name>branch all remain.git worktree prunewon't reap it (the dir is still present). This differs from #67196's hypothesis that the leftover branch is caused by a mid-sessiongit branch -mrename — here no rename is involved; a vanilla create → work → commit → push → exit cycle leaks.
- (Intermittent, severe) Parent-repo
core.barecorruption. In one occurrence the same teardown leftcore.bare = truein the primary repo's shared.git/config. That breaks the whole repo:git status/git worktree/ commit all fail withfatal: this operation must be run in a work tree, and git's "refusing to fetch into a checked-out branch" safety rail is disabled — so a latergit fetch origin main:mainadvanced the ref while leaving the working tree frozen, surfacing in the next session as a phantom "staged revert" of everything that had changed since. It did not recur on clean single cycles; it appears tied to the messy refuse → retry path from symptom 1.
Steps to reproduce (the reliably-reproducible orphan)
EnterWorktree(name mode) → session cwd becomes the worktree root.- Without
cd-ing out, callExitWorktree { action: "remove" }→ refused:Could not verify worktree state … Refusing to remove without explicit confirmation. ExitWorktree { action: "remove", discard_changes: true }→ returnsExited and removed worktree at ….- From the primary checkout, observe the leak:
git worktree liststill lists the worktree- the worktree directory still exists on disk
git branch --list 'worktree-<name>'is still presentgit worktree pruneis a no-op (dir present)
Cleanup requires a manual git worktree remove --force <path> + git branch -D worktree-<name>.
Observed 3× in one session — every time the session cwd was inside the worktree at removal.
What I verified about the core.bare variant
Plain git worktree remove --force <wt> and rm -rf <wt> && git worktree prune run from inside a worktree do not flip core.bare, so the mutation lives in ExitWorktree's own removal path, not in git. The WorktreeCreate hook does a plain git worktree add and never touches core.bare, and there is no custom remove hook.
Expected behavior
ExitWorktree action="remove" should either fully remove the worktree (directory + .git/worktrees/<name> admin entry + branch) or report failure — never (a) claim success while leaving the worktree registered, or (b) mutate the parent repo's core.bare (or any other shared .git/config key).
Workaround
cdto the primary checkout before callingExitWorktree.- After teardown, verify with
git worktree listandgit config core.bare(must befalse), and clean up any orphan manually.
Related issues
- #67196 — same family (EnterWorktree writes
core.hooksPathinto the shared repo config; ExitWorktree doesn't restore it, and notes a branch left behind after a rename). This report adds two things: the orphan happens without any rename, and the shared-config mutation can also hitcore.bare, not justcore.hooksPath. - #48250 — ExitWorktree no-op for Desktop-created worktrees (different cause).
- #62724 — feature request for merge verification on ExitWorktree (related to the "could not verify worktree state" refusal in step 2).
5 Comments
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
Thanks for the triage — but I'd push back on auto-closing this as a stale duplicate, because two of the three flagged issues were closed as completed (fixed) yet the behavior still reproduces on the current build:
ExitWorktree action:"remove"I ran today on v2.1.177 (2026-06-20) — 3 for 3 (refuse →discard_changes:true→ "Exited and removed worktree" success message, but the dir, the.git/worktrees/<name>/admin entry, and the branch all remain). If this was fixed in April, it has regressed.core.barecorruption of the parent repo from a worktree dispatch. That's symptom (2). I hit the samecore.bare=true-on-the-primary corruption, here via anExitWorktreeteardown rather thanAgent isolation:"worktree", but it's the same shared-config corruption.core.bare-not-restored class, declined only 4 days ago.So the disposition across these is inconsistent (one "fixed", one "won't fix") and both behaviors are live on the latest version. What this issue adds beyond the three:
core.bareflip silently disables git's "refusing to fetch into a checked-out branch" safety rail, so a latergit fetch origin <branch>:<branch>advances the ref while leaving the working tree frozen — surfacing in the next session as a phantom "staged revert" of everything that changed since. That's the actual user-facing damage, not just an inoperativegit status. (Note: the orphaned-worktree symptom is 100% reproducible for me; thecore.bareflip is intermittent — observed once, did not recur on three clean teardown cycles.)git branch -mrename that #67196 hypothesized as its trigger.Happy to close this in favor of reopening #47386 / #51930 to track the regression if that's the preferred home — but it shouldn't auto-close as resolved, since it isn't on the current build.
Independent second occurrence of failure 2 (parent-repo
core.barecorruption), on an environment nearly identical to the report.extensions.worktreeConfig = trueand ~15 linked worktrees under.claude/worktrees/What I saw.
core.bareflipped fromfalsetotruein the primary repo's shared.git/config. This broke every checkout in the repo at once — the main one and all linked worktrees:Two details that may help pin down the mechanism:
.git/config.worktreeat the time. If the trigger is git's documentedcore.bare/core.worktreemigration into per-worktree config, it looks like only the shared half of that migration landed — the compensatingcore.bare = falsewas never written anywhere, which is exactly why the repo ended up globally broken instead of merely relocated.git rev-parse --git-dirkept working; only work-tree resolution failed. So the damage is narrowly thecore.bareflag.Recovery is a one-liner, and non-destructive — no uncommitted work was lost in the main checkout:
On the trigger — I can't confirm it. I did not invoke
ExitWorktreein that session, so if teardown is the cause it would have to have been automatic cleanup rather than an explicit removal. The flip appeared within a window in which I was iteratinggit configreads/writes across all worktrees viagit worktree list. I tried to reproduce it afterwards in an isolated throwaway repo —git config --worktreereads and writes, from both the main and a linked worktree, plus aprepare-style install script — and could not, which is consistent with the "intermittent" characterisation here.Adding this mainly as corroboration that it isn't a one-off, and because the blast radius deserves emphasis: this is not confined to the worktree. It takes down the primary checkout and every sibling worktree simultaneously, and the error message (
must be run in a work tree) gives no hint that a config flag is responsible — it reads like repo corruption. Anyone hitting it is likely to reach for far more destructive remedies than the one-line fix above.Corroborating the
core.baresymptom described in Problem 2 on Windows — this issue is currently labeledplatform:macosonly.Claude Code
2.1.226. Foundcore.bareset totrueon a normal, non-bare checkout, breaking everygitcommand in it (fatal: this operation must be run in a work tree) until fixed withgit config --local core.bare false. This followed agit worktree add/removecycle; I can't confirm whether it went through theEnterWorktree/ExitWorktreetools specifically or a manualgit worktreeinvocation, so I'm not claiming to confirm the exact trigger — only that the symptom matches.This is not a single observation: I have 4 documented occurrences of this same symptom on this one machine, tracked internally since 2026-07-30 (mine was the 4th, ~10 days later). Each was fixed the same way. I don't have enough detail on the first 3 to confirm they went through the same trigger as mine, so I still can't call it deterministic — but the frequency (4 times in ~10 days) seems worth knowing.
Separately: prior report #58345 (macOS,
2.1.139) was auto-closed for inactivity, not fixed. I checked the changelog across all 87 versions from2.1.139to2.1.226— no entry addressescore.bare,EnterWorktree, orExitWorktreerestoring config state on exit.Hit failure mode 2 (the
core.barecorruption) today on a repo with 83 worktrees, and I think I can offer a deterministic reproduction for the part described here as intermittent, plus a control that prevents it.Environment: macOS 26.5.1 (Darwin 25.5.0), git 2.50.1 (Apple Git-155), Claude Code 2.1.207. Repo has
extensions.worktreeConfig = true.Deterministic repro
Control — setting
GIT_WORK_TREEprevents it:The mechanism looks like git's name heuristic: a git dir not ending in
/.gitis assumed bare, soinitwritesbare = true. Becauseconfigis a shared path, the write lands in the common config rather than a worktree-local one.In our incident the value also landed at line 4, matching the repro exactly.
Two things that make it worse than it looks
1.
extensions.worktreeConfig = trueturns it from local to total. That extension removes git's normal exception whereby a shared-configcore.barebinds only the main worktree. With it enabled, one write broke the main repo and all 83 linked worktrees simultaneously — everygit status,git commit,git checkoutreturningfatal: this operation must be run in a work tree.2. The obvious repair does not work, and looks like it did. Running
git config --local core.bare falsewrites to.git/config.worktreewhenworktreeConfigis enabled.git config --get core.barethen answersfalseand the main worktree recovers — while the shared config still saystrueand every other worktree stays broken. It reads as fixed and isn't.The repair that works targets the shared file explicitly:
Why it is hard to notice
git commitfatals before hooks run, so no pre-commit hook can catch this state. And any tooling whose entry guard isgit rev-parse --show-toplevel 2>/dev/null || exit 0silently exits —--show-toplevelreturns rc=128 undercore.bare, so detection tooling switches itself off in exactly the state worth reporting.git rev-parse --is-inside-work-tree(returnsfalse, rc=0) andgit config --getboth still work and are safe to use as guards.Suggested fix
Have the worktree tooling invoke git with the environment cleared where it doesn't need inheriting —
env -u GIT_DIR -u GIT_WORK_TREE git …— or always passGIT_WORK_TREEalongsideGIT_DIR. The control above suggests either would close it.Happy to supply more detail if useful.