Worktree isolation silently lost when repo has core.worktree in shared config + extensions.worktreeConfig

Status Fixed / completed
Maintainer reply None cached
Activity 1 comment · opened Jul 17, 2026 · closed Aug 21, 2026

Summary

When a repository has core.worktree in its shared config and extensions.worktreeConfig = true, every linked worktree resolves to the main checkout instead of its own directory. A Claude Code session created in such a worktree believes it is isolated but is actually reading and writing the main checkout.

I hit this in practice: two sessions ended up operating on the same working directory simultaneously. Both committed to main in that directory, and one ran git reset while the other was mid-task. Nothing was lost, but only because the concurrent work happened to be committed already.

Why this combination occurs

Neither ingredient is a mistake on its own:

  • core.worktree in the shared config is normal for a repo with a separated gitdir (git init --separate-git-dir, git clone --separate-git-dir, or a clone repaired after object-store corruption). The .git file points elsewhere and core.worktree names the checkout.
  • extensions.worktreeConfig = true is enabled by tooling that wants per-worktree config.

Together they break isolation, because enabling the extension removes git's exception that confines core.worktree to the main worktree. git-worktree(1) documents this explicitly:

Note that in this file, the exception for core.bare and core.worktree is gone. If they exist in $GIT_DIR/config, you must move them to the config.worktree of the main worktree. - core.worktree should never be shared.

Reproduction

git 2.50.1 (Apple Git-155), macOS. Runs verbatim:

R=/tmp/wt-repro; mkdir -p $R/checkout $R/gitdirs; cd $R
git init -q --separate-git-dir=$R/gitdirs/repo.git checkout
cd checkout && git commit -q --allow-empty -m init

# normal for a separated gitdir:
git config --file $R/gitdirs/repo.git/config core.worktree $R/checkout
# the trigger:
git config --file $R/gitdirs/repo.git/config extensions.worktreeConfig true

git worktree add -q $R/wt -b wt
(cd $R/wt && git rev-parse --show-toplevel)

Actual: /private/tmp/wt-repro/checkout — the main checkout
Expected: /private/tmp/wt-repro/wt — the worktree itself

(/private/tmp is just macOS resolving /tmp; the point is checkout vs wt.)

Control — same repo, extension removed, worktree resolves correctly:

git config --file $R/gitdirs/repo.git/config --unset extensions.worktreeConfig
git worktree add -q $R/wt2 -b wt2
(cd $R/wt2 && git rev-parse --show-toplevel)   # -> /private/tmp/wt-repro/wt2

Why it is easy to miss

The failure is silent. git worktree list shows the worktree at its correct path, the directory exists and is populated with checked-out files, and file-based tooling reading paths under the worktree sees plausible content. Only git rev-parse --show-toplevel from inside the worktree reveals that git operations target the main checkout.

Suggested fix

When creating a worktree, detect core.worktree present in the shared config while extensions.worktreeConfig is enabled, and either:

  1. move core.worktree into the main worktree's config.worktree (the remedy git-worktree(1) prescribes), or
  2. refuse and warn loudly.

The documented remedy is two commands and is safe if ordered so the main worktree is never left without a pointer:

git config --worktree core.worktree /path/to/checkout        # write the new location FIRST
git config --file <gitdir>/config --unset core.worktree      # then remove the shared one

I applied exactly this to the affected repo and verified: the main checkout still resolves, new worktrees isolate correctly, pre-existing broken worktrees recover without recreation, and a file written in a worktree no longer appears in the main checkout.

A session that believes it is isolated but silently shares the user's main checkout is a data-loss hazard — a git reset --hard or branch checkout from one session can destroy another's uncommitted work.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗