[Bug] EnterWorktree/ExitWorktree tools don't restore core.bare config on exit
I have seen this issue for months, but today Claude Code was able to determine why. This has contributed to my hatred of work trees, but I see how they are useful, however Claude Code usage of them must be reliable.
Bug Description
Found the culprit. .git/config mtime is May 12 08:47:14 — minutes ago, right after my ExitWorktree --remove. The Claude Code harness EnterWorktree/ExitWorktree tools flip core.bare = true when you
enter a worktree, and don't restore it on exit. That's not documented in the tool descriptions, but the config mtime nails it.
History: this repo was originally a normal checkout (loose CLAUDE.md from Apr 14, package.json from May 11 09:21 — both still on disk). Every time EnterWorktree ran, the parent flipped to bare.
ExitWorktree(remove) doesn't reset it, presumably because other worktrees (avonos-final, platform-client-dataverse-extraction) exist and worktrees work fine against a bare parent.
The existing files at the root are leftover from before any of those EnterWorktree calls ever happened. They're not tracked while bare = true.
One-line fix (gives you back a normal working tree at this directory; doesn't affect the other worktrees — they work fine either way):
git config --local core.bare false
Want me to run that? After it, git status will work here and the loose files will be visible relative to main.
Environment Info
- Platform: darwin
- Terminal: Apple_Terminal
- Version: 2.1.139
- Feedback ID: 4ff26c9c-b9c6-4182-8bb9-c6dd03947a0b
Errors
[{"error":"Error: NON-FATAL: Lock acquisition failed for /Users/jeffdutton/.local/share/claude/versions/2.1.139 (expected in multi-process scenarios)\n at z16 (/$bunfs/root/src/entrypoints/cli.js:2662:2257)\n at tRH (/$bunfs/root/src/entrypoints/cli.js:2662:1337)\n at processTicksAndRejections (native:7:39)","timestamp":"2026-05-11T23:36:58.241Z"},{"error":"Error: Plugin MCP server error - mcp-config-invalid: MCP server terraform invalid: Missing environment variables: TFE_TOKEN\n at m7H (/$bunfs/root/src/entrypoints/cli.js:2663:36777)\n at processTicksAndRejections (native:7:39)","timestamp":"2026-05-11T23:36:58.277Z"},{"error":"Error: Plugin MCP server error - mcp-config-invalid: MCP server terraform invalid: Missing environment variables: TFE_TOKEN\n at m7H (/$bunfs/root/src/entrypoints/cli.js:2663:36777)\n at async L (/$bunfs/root/src/entrypoints/cli.js:6416:11729)\n at processTicksAndRejections (native:7:39)","timestamp":"2026-05-11T23:36:58.876Z"},{"error":"Error: Plugin MCP server error - mcp-config-invalid: MCP server terraform invalid: Missing environment variables: TFE_TOKEN\n at m7H (/$bunfs/root/src/entrypoints/cli.js:2663:36777)\n at async N (/$bunfs/root/src/entrypoints/cli.js:6416:12615)\n at processTicksAndRejections (native:7:39)","timestamp":"2026-05-11T23:36:58.877Z"},{"error":"Error: Request was aborted.\n at makeRequest (/$bunfs/root/src/entrypoints/cli.js:51:6192)\n at processTicksAndRejections (native:7:39)","timestamp":"2026-05-11T23:42:07.685Z"},{"error":"AxiosError: timeout of 5000ms exceeded\n at <anonymous> (/$bunfs/root/src/entrypoints/cli.js:121:13313)\n at emit (node:events:92:22)\n at <anonymous> (/$bunfs/root/src/entrypoints/cli.js:120:3321)\n at request (/$bunfs/root/src/entrypoints/cli.js:123:2467)\n at processTicksAndRejections (native:7:39)","timestamp":"2026-05-12T12:36:52.398Z"},{"error":"Error: Request was aborted.\n at makeRequest (/$bunfs/root/src/entrypoints/cli.js:51:6192)\n at processTicksAndRejections (native:7:39)","timestamp":"2026-05-12T12:44:08.517Z"},{"error":"Error: Request was aborted.\n at makeRequest (/$bunfs/root/src/entrypoints/cli.js:51:6192)\n at processTicksAndRejections (native:7:39)","timestamp":"2026-05-12T12:46:59.541Z"}]
8 Comments
Found 2 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
Independent repro + workaround
Hit this independently on a different repo (macOS 24.6.0). Different starting state from @jdutton's — mine was a normal non-bare checkout — and the corruption came in through Superpowers'
using-git-worktreesskill (its Step 1a steers Claude Code agents towardEnterWorktreeovergit worktree add, which widens the blast radius beyond users who call EnterWorktree directly).Forensics that line up with @jdutton's diagnosis
The mtime correlation is tighter than I expected:
.git/configrewritten withcore.bare = trueat one timestamp.git/worktrees/<branch>/HEADandindexupdated one second laterSequence: shared-config write, then linked-worktree state update. Not in shell history. Not in Codex session JSONLs either (Codex doesn't have
EnterWorktree, so it can't be the writer). Consistent with anEnterWorktreeinvocation.How this case differs from #45201
Both this issue and @therickfactr's hit a "wrong config scope" defect, but in different forms:
| | #45201 | This issue (#58345) |
| --- | --- | --- |
| Starting layout | Intentional bare-hub (
git clone --bare) | Normal non-bare checkout || What Claude Code writes |
extensions.worktreeConfig = true(re-scopes the existingcore.bare = true) |core.bare = truedirectly to shared config || What breaks | Existing worktrees inherit
bare = truebecause noconfig.worktreeoverrides exist | Parent dir loses its working tree |I checked:
extensions.worktreeConfigis not set, and noconfig.worktreefiles exist under.git/worktrees/*/. So this is the simpler, non-extensions variant. Cross-linking #45201 and #27474 (core.hooksPathto$GIT_COMMON_DIR/config) — they look like three manifestations of the same root cause: the harness writes to shared/common git config when it should write to worktree-local config.Durable workaround (macOS)
@jdutton's one-line fix (
git config --local core.bare false) recurs every timeEnterWorktreeruns. To make the fix stick until the upstream bug is resolved:chflags uchgsets the BSD user-immutable flag — any process attempting to rewrite.git/config(includingEnterWorktree) getsEPERM. That converts the silent corruption into a loud, diagnosable error, and the trap defends against the next call without further intervention.To legitimately edit config later (adding a remote, etc.):
Linux equivalent is
chattr +i .git/config(requires sudo). Windows has no clean equivalent without NTFS ACLs — affected Windows users may need to wait for the upstream fix.Suggested unified fix direction
Across #58345, #45201, and #27474, the right invariant seems to be:
EnterWorktreeshould write only to worktree-local config (git config --worktree <k> <v>) and never to$GIT_COMMON_DIR/config. That requiresextensions.worktreeConfig = truefirst (and per-worktreeconfig.worktreefiles seeded for existing worktrees, per #45201's fix proposal). If that invariant is hard to retrofit, a narrower fix specific to this issue would be: onExitWorktree --remove, if no other linked worktrees exist and the shared config currently hascore.bare = truefrom an EnterWorktree write, restorecore.bare = false. The chflags trap will catch any regression of either invariant.Adding evidence from a different team — same root cause, broader symptom surface.
Three independent reproductions
In ArcadeAI/safeword with 4 active worktrees in
.claude/worktrees/:| Date | Trigger | Failed git op | Recovery |
|------|---------|---------------|----------|
| 2026-05-03 10:37Z | spawned follow-up chip; ref-bump on
claude/ticket-137-138-unified-overridecorrelated 1:1 with bare-flip event |git commit|git config core.bare falseimmediately restored || 2026-05-15 05:44Z | two sibling worktrees actively writing for ~6-11 min |
git status -sbin parent worktree | same one-liner restored || 2026-05-17 (today) | mid-session, during back-to-back work across multiple worktrees |
git status,git mv,git push -u— hit ~5 times in one session | manual reset each time |Symptom surface is broader than EnterWorktree/ExitWorktree
The original report focuses on Enter/Exit not restoring
core.bare. The 2026-05-17 reproduction shows the race firing mid-session, on ad-hoc reads (git status,git mv,git push) — well after any Enter/Exit boundary. Whatever holdscore.bare = truetransiently during worktree-creation can be observed by any git call that races with another worktree's creation/modification, not just the Enter/Exit-adjacent ones.The error message is misleading
fatal: this operation must be run in a work treeblames the user's CWD. The actual cause is a shared-config race in.git/configon the parent. Users naturally chase CWD/path issues for hours before finding the real cause.Workarounds in the wild are partial
We shipped a defensive
core.bare = falsereset inside.husky/pre-commit(commit aeb342b). It catches the race only insidegit commit— and on the 2026-05-17 session, the reset fired correctly during commits while everygit status/git mv/git pushbetween them still failed unprotected. Now extending to a PreToolUse hook onBash(git *)to cover ad-hoc reads.This is a worse fix-cost asymmetry than it looks: every project using Claude Code with worktrees needs its own bespoke mitigation, and even with one, partial coverage is the norm.
Proposed upstream fix (in order of preference)
git worktree add --no-checkout+ post-fixup avoids touchingcore.bareon the parent.git/configduring the toggle so concurrent reads block instead of seeing the bare-true intermediate stateRelated: #51930 (closed) showed the same root cause via the Agent-tool isolation path. So this race affects multiple Claude Code worktree-touching surfaces.
Adding another data point — same bug class, different key.
On Windows 11 / PowerShell 7.4 / Claude Code VSCode extension, every
Agent({ isolation: "worktree", ... })dispatch rewrites the parent repo'score.hooksPathfrom a versioned in-repo path (e.g.tools/git-hooks/) to the absolute legacy.git/hooksdirectory. Reproduced 4/4 times across four independent sub-agent dispatches — sub-agent briefs explicitly forbadegit configcalls, so the rewrite is happening in the harness's worktree-add or teardown step, not in the sub-agent's actions.Impact in our setup: versioned
commit-msg/post-commit/pre-pushhooks all go silently inert in the parent repo. Trailer injection stops, derived-state refresh stops, branch-protection pre-push guards stop firing. No error is emitted —git commitproceeds normally. Only an explicitgit config --get core.hooksPathcheck reveals the drift, which is why we added a session-start sentinel.Workaround: re-run our idempotent hook installer (
git config core.hooksPath <path>) after every isolated sub-agent dispatch.Same root cause as the
core.barecase here and theoriginURL case in #51106 — the worktree harness writes to the parent's shared.git/configand doesn't restore on exit. Suggests the proper fix is general (snapshot+restore the parent config, or operate on the worktree's own config viaextensions.worktreeConfig) rather than per-key.Closing for now — inactive for too long. Please open a new issue if this is still relevant.
@bcherny please consider prioritizing bug fixes over new features so that effort isn't wasted on re-opening un-fixed bugs auto-closed by Github Actions.
@j7an please upvote https://github.com/anthropics/claude-code/issues/30407
This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.