[BUG] `claude --worktree` overwrites the `core.hooksPath` of `$GIT_COMMON_DIR/config`
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
claude --worktree overwrites the core.hooksPath of $GIT_COMMON_DIR/config to $GIT_COMMON_DIR/hooks. This ends up as one of the following:
- If
$GIT_DIR/config-core.hooksPathhasn't been specified, it's effectively no-op. Read https://git-scm.com/docs/githooks - Otherwise, the
core.hooksPathis overwritten, which is _destructive_ (if it points to some non-default path)
I guess that the original intention was to set the hooks for the newly created worktree to the main repo's one. But as I mentioned above, the actual behavior turns out to be merely modifying the $GIT_COMMON_DIR config. If it wants to touch the _worktree-specific_ thing, git config --worktree is the answer (which requires setting another config, though). But even git config --worktree is pointless since, if sharing hooks is the goal, it's already achieved by git-config resolution.
What Should Happen?
core.hooksPath of $GIT_DIR/config must be untouched when Claude Code creates a worktree internally (either by claude --worktree or EnterWorktree tool).
Error Messages/Logs
Steps to Reproduce
- Start with any repo with a custom
core.hooksPathpointing to anything other than$GIT_DIR/hooks cdinto the repo and executeclaude --worktree- Overwritten.
Claude Model
None
Is this a regression?
No, this never worked
Last Working Version
_No response_
Claude Code Version
2.1.50
Platform
Anthropic API
Operating System
macOS
Terminal/Shell
iTerm2
Additional Information
_No response_
Showing cached comments. Read the full discussion on GitHub ↗
12 Comments
not stale
Seconding this. The worktree setup function is checking for
.huskyand.git/hooks. It does not check whethercore.hooksPathis already configured.Not fixed yet in v2.1.114
Adding empirical confirmation — Claude Code 2.1.140 on Windows 11, Git 2.35.1.windows.2. Three independent reproductions across separate sessions over three days (2026-05-11, 2026-05-12, 2026-05-13), each from a clean
.githooksstate.Minimal repro on Windows:
git config core.hooksPath .githooksin the primary clone (relative path)git config --get core.hooksPath→.githooksEnterWorktree(name: "test")git config --get core.hooksPath→C:\Claude\datalake\main\.git\hooks(Windows-backslash absolute path to git's default hooks dir)The drift value is consistently the Windows-backslash absolute form (
C:\...\.git\hooks, not/c/.../.git/hooks), suggesting a Windows-native code path is writing to.git/config.Downstream impact: our
.githooks/pre-commitenforces TLS-cert verification, secret scanning, schema-drift checks, RLS on new tables, and migration idempotency. Whencore.hooksPathpoints at a stale absolute path, git silently runs zero hooks (verified — invalidcore.hooksPathdoes NOT fall back to.git/hooks). In one drift event, ~25 commits bypassed the entire local chain before CI caught a TLS violation the local hook would have rejected.Local workaround we shipped: auto-correct the drift in our post-EnterWorktree bootstrap script.
Contains practical impact for us. Still creates an exposure window between
EnterWorktreeand the bootstrap step. Would appreciate either (a) preserving a pre-existing customcore.hooksPathvalue duringEnterWorktree's setup, or (b) scoping the change viaextensions.worktreeConfigso the write doesn't leak into the shared.git/config.The workaround we're using involves adding a
WorktreeCreatehook that mimics what Claude's default hook seems to do, minus the bug:Add this to
.claude/settings.json:And the contents of
create-claude-worktree.sh:Not yet fixed in v2.1.146
Additional repro:
Agenttool withisolation: "worktree"on LinuxConfirming this also triggers on Linux via the
Agenttool when a skill spawns sub-agents withisolation: "worktree". The write goes to the main repo's.git/config, not aworktree-specific config.
How we confirmed: Replaced
/usr/bin/gitwith a wrapper that logs calls containingcore.hooksPath. Caught:caller: claude --plugin-dir ... /ai-council-review:ai-council-review head
args: git config core.hooksPath /path/to/repo/.git/hooks
The write is atomic (
.git/config.lock→.git/configrename), so watching the file with inotify misses it — you needIN_MOVED_TOon the parent directory.Impact:
pre-commit installhard-fails with "Cowardly refusing to install hooks withcore.hooksPathset" after every agent session, breaking any setup script that calls it.Workaround:
SessionStarthook in.claude/settings.local.json:```json
{
"hooks": {
"SessionStart": [{
"hooks": [{
"type": "command",
"command": "git -C /path/to/repo config --unset-all core.hooksPath 2>/dev/null; pre-commit install -f"
}]
}]
}
}
Still present in v2.1.163 (Linux, WSL2). Adding two things this thread doesn't have yet: a deterministic minimal repro with timestamps, and the actual code path inside the binary that causes it — including why it fires on repos that don't use husky at all.
Repro (deterministic, 2/2)
Repo config:
core.hooksPath = .githooks(relative, tracked hooks dir — the documentedgit config core.hooksPath .githookssetup).Watchdog: 1-second poll logging
git config core.hooksPath+.git/configmtime, then twoEnterWorktreetool calls with a restore in between:Flips within seconds of each
EnterWorktree, every time.ExitWorktreenever restores it. Until the user notices, the repo's tracked hooks (in our case a commit-msg author guard and a pre-push lint/typecheck gate) are silently disabled for the main checkout and all sessions.The code path (extracted from the v2.1.163 binary)
The worktree-setup routine that runs after
git worktree add(minified; variables renamed):Three compounding problems:
.git/hooksfallback is always armed..git/hooksexists in every git repository (sample hooks), sohooksSourceis always truthy even in repos with no husky and no custom hook manager. This is why the bug hits everyone, not just husky users.core.hooksPath(the common, documented form): it string-compares.githooksto an absolute path. So the write re-applies on every worktree creation — restoring the config only protects you until the nextEnterWorktree(ours flipped back within 20 minutes, from a parallel session's worktree).git configwithout--worktree(onlycwddiffers) writes$GIT_COMMON_DIR/config— shared by the main checkout and every worktree — exactly as the OP diagnosed.Why the write is unnecessary in the first place
For the non-husky case the write is a no-op at best (
.git/hooksis git's default) and destructive at worst (clobbers a custom value). For relative values like.githooks, git already resolves the path against each worktree's own checkout — which contains the tracked hooks — so worktrees get working hooks with no configuration at all. The only arguably-legitimate case is an untracked absolute hooks dir (husky's.husky/_), and that one already resolves correctly from the shared config without rewriting it.Suggested fix: drop the
.git/hooksfallback entirely, and skip the write when the existingcore.hooksPathis a relative path (it already works in worktrees). If a per-worktree override is ever truly needed, it must begit config --worktreebehindextensions.worktreeConfig, never the shared config.Also confirming @CullerierG's report: we see the same write from the
Agenttool withisolation: "worktree", and our incident review shows the earlier flips came from background sessions creating worktrees — which makes this especially nasty in multi-session/agent-heavy workflows: any session entering a worktree disarms hooks for all of them.Funny how this tool thinks it has any business just overwriting your git config values, and does not even leave an escape hatch to disable that behavior
This is absurd, I tried to using work trees today and all my hooks exploded. This took me forever to figure out what was going on. Insane to have a tool silently editing these configs.
Some binary archaeology that this thread doesn't have yet: the behavior changed in v2.1.179 — the destructive
.git/hookspinning is gone, but the silent shared-config write is still there in current versions.I extracted the worktree post-create setup function from published binaries (
@anthropic-ai/claude-code-darwin-arm64from npm, located by searching for the log stringConfigured worktree to use hooks from main repository):<main>/.husky,<main>/.git/hooks] — and.git/hooksalways exists — string-compares it to the configuredcore.hooksPath, and on mismatch runsgit config core.hooksPath <main>/.git/hookswith cwd = the new worktree. Plaingit configfrom a linked worktree writes to$GIT_COMMON_DIR/config, so a repo configured withcore.hooksPath = .githooksgets it replaced by the absolute, empty default — hooks silently die repo-wide. This is the destructive variant everyone in this thread has hit (matches @emw3's code-path analysis of 2.1.163)..husky/.git/hooksfallback is gone. The function now only acts when the configured value is relative: it absolutizes it against the main repo root and writes that back — still via plaingit config(no--worktree, noextensions.worktreeConfig), still into the shared config. Socore.hooksPath = .githooksbecomescore.hooksPath = /abs/path/to/repo/.githooks. Hooks keep firing (worktrees now execute the main checkout's copy), but the tool is still silently mutating the user's repo config, and anything that expects the literal relative value (validators, dotfile sync, docs) breaks.Observed in the wild across a multi-repo portfolio (macOS, 2.1.205–207): every repo that ever hosted a worktree-isolated agent and never set
core.hooksPathnow carrieshooksPath = <repo>/.git/hooks(fingerprint of the old variant — functionally a no-op, but nobody wrote that by hand); the one repo using a relative tracked.githookshad it rewritten to the absolute form.Workaround that survives current versions: configure the absolute path yourself —
git config core.hooksPath "$(git rev-parse --show-toplevel)/.githooks". The current function short-circuits when the stored value is already absolute, so it never writes.Suggested fix: scope the write per-worktree (
extensions.worktreeConfig+git config --worktree), or skip entirely whencore.hooksPathis already configured — a relative value pointing at a tracked hooks dir already resolves correctly from linked worktrees.Related open issues describing the same write: #66993, #67196, #67914, #72714.
Following up on the
git config --worktreesuggestion in this issue: it appears to have shipped, and it moved the bug rather than fixing it.On Claude Code 2.1.217 (macOS, Darwin 25.5.0, git 2.50.1), worktree creation no longer writes to the shared
$GIT_COMMON_DIR/config— that half is genuinely fixed, and I can confirm the sharedcore.hooksPathwas left correct and untouched. It now writes into.git/worktrees/<name>/config.worktreeinstead, but still as an absolute path to the main checkout's working tree.For repos that keep hooks in-tree (
core.hooksPath = .githooks, the committed-hooks pattern), the new behaviour is worse than the old one:config.worktreewins on read, so the obvious repair —git config core.hooksPath .githooks— lands in.git/configand is silently shadowed. You can "fix" it repeatedly and change nothing.git config --worktreeis required to touch the file that actually decides, which is not where anyone thinks to look.Point 3 is what makes this hard to diagnose:
.git/configreads correctly the whole time.Observed impact in one repo: a pre-push gate was dead in 7 of 7 worktrees created after the repo adopted
core.hooksPath, while the 7 created before it were fine — the pin only appears once there is a value to copy. The main checkout happened to sit on a branch predating the hooks directory, so every one of those worktrees pushed completely ungated for five days. The failure is entirely silent; it was found only by noticing the hook's banner line missing from a push.Suggested fix: a relative
core.hooksPathalready resolves correctly per worktree — I verified this with real pushes from a linked worktree. So for relative values the copy step is unnecessary, and resolving them to absolute is precisely what breaks them. Either skip the copy when the configured value is relative, or write it relative to each worktree's own root rather than the main checkout's.Workaround for anyone hitting this, since the pin is re-asserted on every new worktree: clear it from a repo-side install script, e.g. in a
prepare/postinstall step —(The
--unsetmust come first, and both need guarding —--worktreeerrors whenextensions.worktreeConfigisn't enabled, e.g. in a plain clone or a CI checkout.)Corroborating this on macOS, with an additional manifestation worth flagging for whoever picks up the fix:
When the clone has
extensions.worktreeConfig=true, the samegit config core.hooksPath <absolute>write (cwd = the new worktree) does not land in the shared.git/configthe way the other reports describe — it lands worktree-scoped, in.git/worktrees/<name>/config.worktree. So instead of a single shared-config clobber, you get a per-worktree stale absolute override that independently shadows the correct shared value in every generated worktree. Same root cause, but a fix that only guards the shared-config path would miss this variant.Also observed: that
config.worktreeis co-written withcore.longpaths=trueat creation. A plaingit worktree addwrites neither key, which is what let me attribute the write to the tool rather than to git.Context: Claude Code worktrees under
<repo>/.claude/worktrees/*, git 2.50.1, macOS. As #66993 / #72714 already conclude, the correct behavior is to write the relative form (which git resolves per-worktree) or to leavecore.hooksPathuntouched entirely.