Worktree creation rewrites the repo's shared core.hooksPath to an absolute path
Environment: Claude Code CLI on Linux; repo with core.hooksPath deliberately pinned to the relative form .githooks.
Summary: Creating a worktree through Claude Code — the EnterWorktree tool or an Agent spawn with isolation: "worktree" — silently rewrites the shared .git/config's core.hooksPath from .githooks to the absolute /home/<user>/<repo>/.githooks. The config is shared by every worktree of the repo, and the change persists after the worktree is removed.
Repro:
git config core.hooksPath .githooks; confirm withgit config --show-origin core.hooksPath.- In a Claude Code session, spawn any subagent with
isolation: "worktree"(a trivial read-only task suffices), or call EnterWorktree. - Re-check: the value is now absolute, and
.git/config's mtime matches the worktree-creation second.
Evidence from a controlled run: relative form restored and verified at 03:10:27; a deliberately trivial read-only probe agent spawned with isolation: worktree (its transcript contains no git-config commands at all); .git/config mtime jumped to 03:15:23.799 — the same second the worktree was created — with the value absolute again. Observed four times on this machine, each correlated with harness worktree creation; other writers (other sessions, root processes, docker binds) ruled out by sweep.
Impact: repos pin the relative form so hooks resolve identically across clones and worktrees and survive a checkout rename. The silent flip breaks that: an absolute path goes stale when the checkout moves, defeats hook-integrity guards that assert the pinned form, and turns any repo-level hook policy (make install-hooks, husky-style) into a restore treadmill.
Expected: worktree creation should not modify core.hooksPath — worktrees inherit the shared config and no rewrite is needed for hooks to resolve — or at minimum should preserve the existing value verbatim.
Showing cached comments. Read the full discussion on GitHub ↗
3 Comments
Reproduced on Windows 11, on two installs: standalone CLI 2.1.236 (
~/.local/bin, native installer) and the Desktop-app-embedded CLI 2.1.234 (%APPDATA%\Claude\claude-code). Adding the pieces this issue doesn't have yet: where the write comes from in the bundle, a live capture of the flip, and a nastier variant when the creating session runs in a secondary worktree.Where it happens (from the shipped bundle, both versions):
The routine resolves the main repo's
core.hooksPathto an absolute path (a), compares it with the worktree's effective value (s), and writes on mismatch. Two problems compound:core.hooksPath(the portable form),sresolves against the new worktree whilearesolves against the main tree, sos !== ais always true and the rewrite fires on every worktree creation.git config core.hooksPath <absolute>withcwdin the new worktree but without--worktree, so it lands in the shared$GIT_COMMON_DIR/configand changes hook resolution for the main tree and every other worktree at once.Live capture (FileSystemWatcher on
.git, EnterWorktree issued from a session):Control: plain
git worktree addon the same repo creates noconfig.worktreeand leaves the shared config untouched. The transcript of the creating session contains no git-config commands; the write is the harness's own.Secondary-tree variant (worse than a broken guard): if the creating session runs in another worktree of the same repo (e.g. a deploy tree at
C:\work\repo-deploysharing the same.git), the absolute path written points at that tree's.githooks. New agent worktrees then execute hooks from a different checkout, potentially a different revision, of the hooks. On this machine 22 of 98 registered worktrees were also born with an absolutecore.hooksPathseeded into their per-worktreeconfig.worktree, 2 of them pointing at the deploy tree.Suggested fix: write with
--worktree(enablingextensions.worktreeConfigif needed), or keep the original relative value, or skip entirely when the repo-level value already resolves correctly per-worktree, which is exactly what a relativecore.hooksPathdoes.Likely same root cause as #72714 and #27474.
Independent Linux confirmation on Claude Code 2.1.220: in a repository that deliberately sets
core.hooksPath=.githooks, Claude-managed linked-worktree creation repeatedly changed the shared value to an absolute path. In a heavily concurrent agent workflow, this can make one worktree execute hook files from a different checkout and therefore a different revision.Additional observations
extensions.worktreeConfigwas unset while 203 linked worktrees still existed. Concurrent setup and cleanup make that state unstable, but the bundle evidence already recorded on this issue identifies the more direct defect: the hooks-path write lacks--worktree, so a plaingit config core.hooksPath ...targets shared configuration regardless of the invoking worktree.Configured worktree to use hooks from main repository:described above..githooks, which masked the recurrence: the setting appeared to fix itself and then drifted again on later worktree creation.The local mitigation is to detect and restore the required relative value, but that only narrows the exposure window. Preserving a valid relative value—or making any necessary propagation explicitly
--worktree-scoped without racing repository-wide worktree configuration—is still the needed upstream fix.Still reproducing on Claude Code 2.1.228 (macOS 26.5.2, git 2.50.1 Apple Git-155) — adding a second platform, a forensic timeline from a multi-session repo, one new diagnostic detail that may localize the fix, and a userland mitigation for affected repos.
Fresh controlled repro (twice, same-second correlation): with the shared config verified at
core.hooksPath = .githooks, a singleEnterWorktreecall flipped the shared.git/configtocore.hooksPath = /Users/<user>/<repo>/.githookswithin the same second, both times. The probe worktrees were used for nothing else.ExitWorktree(action: remove) deletes the worktree and branch but does not restore the value — explicitly checked after exit, still absolute.New diagnostic detail — two creation flows write to two different scopes:
EnterWorktree(and, per the original report,Agentwithisolation: "worktree") writes the absolute value into the shared.git/config— the harmful case, since it mutates state for the main checkout and every other worktree..git/worktrees/<name>/config.worktreegetscore.hooksPath = <abs>pluscore.longpaths = true(observed on several worktrees, each file's mtime matching that session's start to the second; that variant is #88747). This flow does not clobber the shared config.So a correctly-scoped write path already exists in the product; routing the
EnterWorktreepath through the same worktree-scoped write (or simply not touchinghooksPathat all — the relative value resolves fine in linked worktrees when the hooks dir is committed) would fix the shared-config clobber. Related open reports of the same class, for dedupe: #72714, #27474 (shared config), #88747 (worktree-scoped variant).Measured impact in a repo where many sessions share one clone: our ship gate asserts the pinned relative form. Over ~2 weeks it flapped in a dozen+ sessions' transcripts; on one day alone we restored the value three times and each regression correlated with a harness worktree creation (one at
09:19:06Zlanded inside a 24-minute window bounded by two gate readings — pass before, fail after). Every session that used a worktree silently broke the gate for all sessions that followed.Userland mitigation until this is fixed, for anyone whose hook-policy check is flapping: accept an absolute value only when it resolves to exactly
<main-checkout-root>/.githooks, deriving the root fromgit rev-parse --path-format=absolute --git-common-dir(parent dir). That stays machine-correct — on any other machine the stored absolute no longer equals that machine's root, so the portability warning still fires where it is real — and keeps foreign/absolute-to-.git/hooksvalues failing:Expected behavior (agreeing with the OP): worktree creation should not modify the shared
core.hooksPath; if the product needs a per-worktree value,git config --worktreeis the right scope — and exit/removal should leave no residue either way.