[BUG] ExitWorktree (remove) reports success but orphans the worktree (dir + admin entry + branch); intermittently corrupts parent repo core.bare

Status Open
Reported on v2.1.177
Maintainer reply None cached
Activity 5 comments · opened Jun 20, 2026

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". A WorktreeCreate hook 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:

  1. (100% reproducible) Orphaned worktree despite a success message. The first remove is refused (Could not verify worktree state … Refusing to remove without explicit confirmation); after re-invoking with discard_changes: true it returns Exited and removed worktree at …, but the worktree directory, its .git/worktrees/<name>/ admin entry, and the worktree-<name> branch all remain. git worktree prune won't reap it (the dir is still present). This differs from #67196's hypothesis that the leftover branch is caused by a mid-session git branch -m rename — here no rename is involved; a vanilla create → work → commit → push → exit cycle leaks.
  1. (Intermittent, severe) Parent-repo core.bare corruption. In one occurrence the same teardown left core.bare = true in the primary repo's shared .git/config. That breaks the whole repo: git status / git worktree / commit all fail with fatal: 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 later git fetch origin main:main advanced 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)

  1. EnterWorktree (name mode) → session cwd becomes the worktree root.
  2. Without cd-ing out, call ExitWorktree { action: "remove" } → refused: Could not verify worktree state … Refusing to remove without explicit confirmation.
  3. ExitWorktree { action: "remove", discard_changes: true } → returns Exited and removed worktree at ….
  4. From the primary checkout, observe the leak:
  • git worktree list still lists the worktree
  • the worktree directory still exists on disk
  • git branch --list 'worktree-<name>' is still present
  • git worktree prune is 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

  • cd to the primary checkout before calling ExitWorktree.
  • After teardown, verify with git worktree list and git config core.bare (must be false), and clean up any orphan manually.

Related issues

  • #67196 — same family (EnterWorktree writes core.hooksPath into 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 hit core.bare, not just core.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).

View original on GitHub ↗

5 Comments

github-actions[bot] · 2 months ago

Found 3 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/47386
  2. https://github.com/anthropics/claude-code/issues/51930
  3. https://github.com/anthropics/claude-code/issues/58345

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

jsolly · 2 months ago

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:

  • #47386 (closed COMPLETED, 2026-04-13) — "ExitWorktree remove reports success but leaves worktree directory and branch on disk." That's symptom (1) here, and the repro is identical. I hit it on every 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.
  • #51930 (closed COMPLETED, 2026-04-22) — core.bare corruption of the parent repo from a worktree dispatch. That's symptom (2). I hit the same core.bare=true-on-the-primary corruption, here via an ExitWorktree teardown rather than Agent isolation:"worktree", but it's the same shared-config corruption.
  • #58345 (closed NOT_PLANNED, 2026-06-16) — the same 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:

  1. Current repro on v2.1.177 for both symptoms — i.e. regression evidence against the two COMPLETED closures.
  2. The two symptoms share one teardown path and a downstream consequence the older issues don't connect. The core.bare flip silently disables git's "refusing to fetch into a checked-out branch" safety rail, so a later git 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 inoperative git status. (Note: the orphaned-worktree symptom is 100% reproducible for me; the core.bare flip is intermittent — observed once, did not recur on three clean teardown cycles.)
  3. The orphan reproduces without the mid-session git branch -m rename 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.

versalarchitect · 1 month ago

Independent second occurrence of failure 2 (parent-repo core.bare corruption), on an environment nearly identical to the report.

  • Claude Code 2.1.217, macOS (Darwin 25.5.0), git 2.50.1 (Apple Git-155)
  • Repo with extensions.worktreeConfig = true and ~15 linked worktrees under .claude/worktrees/

What I saw. core.bare flipped from false to true in the primary repo's shared .git/config. This broke every checkout in the repo at once — the main one and all linked worktrees:

$ git status
fatal: this operation must be run in a work tree
$ git rev-parse --is-inside-work-tree
false
$ git rev-parse --git-dir      # still resolves fine
.../.git/worktrees/<name>

Two details that may help pin down the mechanism:

  • The main worktree had no .git/config.worktree at the time. If the trigger is git's documented core.bare/core.worktree migration into per-worktree config, it looks like only the shared half of that migration landed — the compensating core.bare = false was never written anywhere, which is exactly why the repo ended up globally broken instead of merely relocated.
  • git rev-parse --git-dir kept working; only work-tree resolution failed. So the damage is narrowly the core.bare flag.

Recovery is a one-liner, and non-destructive — no uncommitted work was lost in the main checkout:

git config --local core.bare false

On the trigger — I can't confirm it. I did not invoke ExitWorktree in 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 iterating git config reads/writes across all worktrees via git worktree list. I tried to reproduce it afterwards in an isolated throwaway repo — git config --worktree reads and writes, from both the main and a linked worktree, plus a prepare-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.

kphutt · 21 days ago

Corroborating the core.bare symptom described in Problem 2 on Windows — this issue is currently labeled platform:macos only.

Claude Code 2.1.226. Found core.bare set to true on a normal, non-bare checkout, breaking every git command in it (fatal: this operation must be run in a work tree) until fixed with git config --local core.bare false. This followed a git worktree add/remove cycle; I can't confirm whether it went through the EnterWorktree/ExitWorktree tools specifically or a manual git worktree invocation, 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 from 2.1.139 to 2.1.226 — no entry addresses core.bare, EnterWorktree, or ExitWorktree restoring config state on exit.

Laney99 · 11 days ago

Hit failure mode 2 (the core.bare corruption) 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

d=$(mktemp -d); cd "$d"
git init -q . && echo x > f && git add f && git -c user.email=t@t -c user.name=t commit -qm base
git worktree add -q wt1 -b wt1branch

git config --file .git/config --get core.bare        # -> false

GIT_DIR="$PWD/.git/worktrees/wt1" git init -q        # no GIT_WORK_TREE

git config --file .git/config --get core.bare        # -> true
grep -n bare .git/config                             # -> 4:  bare = true

Control — setting GIT_WORK_TREE prevents it:

git config --file .git/config --unset core.bare
GIT_DIR="$PWD/.git/worktrees/wt1" GIT_WORK_TREE="$PWD/wt1" git init -q
git config --file .git/config --get core.bare        # -> false

The mechanism looks like git's name heuristic: a git dir not ending in /.git is assumed bare, so init writes bare = true. Because config is 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 = true turns it from local to total. That extension removes git's normal exception whereby a shared-config core.bare binds only the main worktree. With it enabled, one write broke the main repo and all 83 linked worktrees simultaneously — every git status, git commit, git checkout returning fatal: 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 false writes to .git/config.worktree when worktreeConfig is enabled. git config --get core.bare then answers false and the main worktree recovers — while the shared config still says true and every other worktree stays broken. It reads as fixed and isn't.

The repair that works targets the shared file explicitly:

git config --file "$(git rev-parse --git-common-dir)/config" --unset core.bare
git config --show-origin --get-all core.bare   # confirm no source says true

Why it is hard to notice

git commit fatals before hooks run, so no pre-commit hook can catch this state. And any tooling whose entry guard is git rev-parse --show-toplevel 2>/dev/null || exit 0 silently exits — --show-toplevel returns rc=128 under core.bare, so detection tooling switches itself off in exactly the state worth reporting. git rev-parse --is-inside-work-tree (returns false, rc=0) and git config --get both 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 pass GIT_WORK_TREE alongside GIT_DIR. The control above suggests either would close it.

Happy to supply more detail if useful.