claude-code - ✅(Solved) Fix Claude Code agent harness writes broken per-worktree core.hooksPath [1 pull requests, 2 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
anthropics/claude-code#60620Fetched 2026-05-20 03:53:48
View on GitHub
Comments
2
Participants
2
Timeline
13
Reactions
0
Author
Participants
Timeline (top)
labeled ×4commented ×2cross-referenced ×2mentioned ×2

Error Message

Error Messages/Logs

Root Cause

  1. It is an absolute path pointing at the main repo's hooks directory, not the worktree's. The worktree lives at D:\projects\invoiceflow-AI\.claude\worktrees\awesome-thompson-7c0a94\, but hooksPath points back at D:\projects\invoiceflow-AI\frontend\.husky\_.
  2. It is written even when the shared $GIT_COMMON_DIR/config already specifies a correct relative core.hooksPath (in our case frontend/.husky/_, set by Husky's prepare script). The per-worktree value silently overrides the shared one because extensions.worktreeConfig = true is enabled by the same harness, and worktree-scoped config wins.

Fix Action

Fixed

PR fix notes

PR #254: examples: worktree-hooks-path-fix.sh — Husky hooks restored in Claude worktrees (#60620)

Description (problem / solution / changelog)

  • New SessionStart hook addressing Issue #60620: Claude Code's EnterWorktree harness writes an absolute core.hooksPath into config.worktree, silently overriding the project's correct shared core.hooksPath (typically the Husky / lefthook / pre-commit setup)
  • Hook unsets the bogus worktree-scoped core.hooksPath only when it is an absolute path pointing outside the worktree root; leaves relative paths and intra-worktree absolute paths alone
  • Only fires inside paths matching .claude/worktrees/ — main checkouts and other worktree locations untouched
  • Disable via CC_WORKTREE_HOOKS_FIX_DISABLE=1 The failure shape: project author installs Husky 9 with core.hooksPath = frontend/.husky/_ in the shared config. Husky's prepare script puts it there. The shared path resolves correctly from any worktree. Then Claude's EnterWorktree writes core.hooksPath = <absolute path to main repo's .husky> into the per-worktree config.worktree, and with extensions.worktreeConfig = true (also written by the harness), the worktree-scoped value wins. Inside the worktree, git resolves the absolute path to the main repo, where the file may not exist (silently runs zero hooks) or where it does exist but runs against the wrong context (binary resolution drifts to the main checkout's node_modules). Either way, the project author's pre-commit chain — secret-scanning, lint, generated-file checks, conventional-commits validation — is silently bypassed for every commit made from a Claude-created worktree. No warning surfaced. This hook is the operator-side restoration of the project author's intent until the harness fix lands.
  • 9 cases pass: bogus absolute path unset, shared config falls through correctly, relative path preserved, intra-worktree absolute preserved, main checkout untouched, non-Claude worktree path pattern untouched, CC_WORKTREE_HOOKS_FIX_DISABLE=1 bypass, non-git directory no-op, empty event payload no-op
  • bash -n syntax check passes
  • Tests use mktemp -d for full isolation; setup creates real git repos with git worktree add
  • workspace-lease-guard.sh (PR #252, 2026-05-20) — different worktree failure (sibling-session git checkout clobber, Issue #60475)
  • concurrent-edit-lock.sh (existing) — per-file Edit/Write lock, not worktree-scoped
  • max-concurrent-agents.sh (existing) — caps subagent fan-out This hook is harness-bug-class (fires before any model interaction), distinct from the recognition-without-arrest model-drift cluster. Reply that prompted the implementation: https://github.com/anthropics/claude-code/issues/60620#issuecomment-4490879785 🤖 Generated with Claude Code

Changed files

  • README.md (modified, +1/-1)
  • examples/README.md (modified, +1/-1)
  • examples/worktree-hooks-path-fix.sh (added, +111/-0)
  • tests/worktree-hooks-path-fix.test.sh (added, +179/-0)

Code Example

[core]
    longpaths = true
    hooksPath = D:\\projects\\invoiceflow-AI\\frontend\\.husky\\_

---

cat $GIT_COMMON_DIR/worktrees/<name>/config.worktree

---

$ git config --worktree --list
core.longpaths=true
core.hookspath=D:\projects\invoiceflow-AI\frontend\.husky\_

$ git rev-parse --git-path hooks
D:\projects\invoiceflow-AI\frontend\.husky\_

$ test -d D:/projects/invoiceflow-AI/.claude/worktrees/awesome-thompson-7c0a94/frontend/.husky/_ && echo EXISTS || echo MISSING
MISSING

$ test -d D:/projects/invoiceflow-AI/frontend/.husky/_ && echo EXISTS || echo MISSING
EXISTS

---

$ for w in affectionate-cray-18805d awesome-thompson-7c0a94 cool-solomon-082b06 cranky-mirzakhani-663c1b distracted-hamilton-3bbfec; do
    echo "--- $w ---"
    cat $GIT_COMMON_DIR/worktrees/$w/config.worktree
  done

--- affectionate-cray-18805d ---
[core]
    longpaths = true
    hooksPath = D:\\projects\\invoiceflow-AI\\frontend\\.husky\\_
--- awesome-thompson-7c0a94 ---
[core]
    longpaths = true
    hooksPath = D:\\projects\\invoiceflow-AI\\frontend\\.husky\\_
--- cool-solomon-082b06 ---
[core]
    longpaths = true
    hooksPath = D:\\projects\\invoiceflow-AI\\frontend\\.husky\\_
--- cranky-mirzakhani-663c1b ---
[core]
    longpaths = true
    hooksPath = D:\\projects\\invoiceflow-AI\\frontend\\.husky\\_
--- distracted-hamilton-3bbfec ---
[core]
    longpaths = true
    hooksPath = D:\\projects\\invoiceflow-AI\\frontend\\.husky\\_

---

[core]
    repositoryformatversion = 0
    ...
    hooksPath = frontend/.husky/_
[extensions]
    worktreeConfig = true

---

-# Currently in $GIT_COMMON_DIR/worktrees/<name>/config.worktree
-[core]
-    longpaths = true
-    hooksPath = <absolute path to main repo's hooks dir>
+# Proposed:
+[core]
+    longpaths = true
+# (omit core.hooksPath entirely — let it resolve from the shared config)
RAW_BUFFERClick to expand / collapse

Preflight Checklist

  • I have searched existing issues and this hasn't been reported yet. Related but distinct: #27474 (writes to shared $GIT_COMMON_DIR/config) and #30251 (duplicate of #27474, closed). This report covers a different file and a different failure mode — see "Relationship to #27474" below.
  • This is a single bug report.
  • I am using the latest version of Claude Code.

What's Wrong?

When Claude Code creates a worktree, the harness writes the following into the per-worktree config file ($GIT_COMMON_DIR/worktrees/<name>/config.worktree):

[core]
    longpaths = true
    hooksPath = D:\\projects\\invoiceflow-AI\\frontend\\.husky\\_

Two issues with the hooksPath value:

  1. It is an absolute path pointing at the main repo's hooks directory, not the worktree's. The worktree lives at D:\projects\invoiceflow-AI\.claude\worktrees\awesome-thompson-7c0a94\, but hooksPath points back at D:\projects\invoiceflow-AI\frontend\.husky\_.
  2. It is written even when the shared $GIT_COMMON_DIR/config already specifies a correct relative core.hooksPath (in our case frontend/.husky/_, set by Husky's prepare script). The per-worktree value silently overrides the shared one because extensions.worktreeConfig = true is enabled by the same harness, and worktree-scoped config wins.

Net effect for any project using Husky (or any tool that installs a repo-relative core.hooksPath): hooks become dormant inside Claude-created worktrees. Git follows the absolute path, and one of two things happens:

  • The absolute path doesn't exist in the worktree (most common, since npm install typically only runs in the worktree's own frontend/ and the main checkout's frontend/.husky/_ may not exist or be stale). Git silently runs zero hooks — core.hooksPath does not fall back to anything when its target is missing.
  • The absolute path does exist in the main checkout. Git runs hooks from the main repo's directory while operating on the worktree's index. Binary resolution (npx, lint-staged, project-local Node version) drifts to wherever the main checkout's node_modules resolves, not the worktree's. Hooks "fire" but against the wrong context.

Either way, the protection chain the project author installed is bypassed for every PR generated from a Claude-created worktree, with no warning surfaced to the user.

What Should Happen?

One of the following:

  • Preferred: Do not write core.hooksPath into the per-worktree config at all. If the shared .git/config has a relative core.hooksPath (as Husky and most repo-hook tools install it), git already resolves it correctly per-worktree by interpreting the path relative to the worktree root — no override is needed.
  • Acceptable: If the harness must write a path (e.g., to handle bare-checkout edge cases), write a worktree-relative value or an absolute path resolved against the actual worktree root, not the main repo's root.

Error Messages/Logs

None. The failure is silent.

Steps to Reproduce

  1. Start with any repo whose shared .git/config already has core.hooksPath set to a repo-relative path (typical of Husky 9+: frontend/.husky/_ or .husky/_).
  2. From a Claude Code session, create a worktree (e.g., via the harness's automatic worktree-on-task behavior, claude --worktree, or the EnterWorktree tool).
  3. Inspect the per-worktree config:
    cat $GIT_COMMON_DIR/worktrees/<name>/config.worktree
  4. Observe:
    • core.hooksPath is set to an absolute path.
    • The absolute path is derived from the main repo's directory, not the worktree's.
  5. From the worktree, run git config core.hooksPath and git rev-parse --git-path hooks to confirm the worktree-scoped value overrides the shared one.
  6. Attempt a commit from the worktree. Husky's pre-commit does not fire (or fires from the main checkout's context, depending on whether the main frontend/.husky/_ directory happens to exist on disk).

Evidence

Run from inside a Claude-created worktree at D:\projects\invoiceflow-AI\.claude\worktrees\awesome-thompson-7c0a94\:

$ git config --worktree --list
core.longpaths=true
core.hookspath=D:\projects\invoiceflow-AI\frontend\.husky\_

$ git rev-parse --git-path hooks
D:\projects\invoiceflow-AI\frontend\.husky\_

$ test -d D:/projects/invoiceflow-AI/.claude/worktrees/awesome-thompson-7c0a94/frontend/.husky/_ && echo EXISTS || echo MISSING
MISSING

$ test -d D:/projects/invoiceflow-AI/frontend/.husky/_ && echo EXISTS || echo MISSING
EXISTS

The hooks resolve into the main checkout's husky directory while the worktree's own frontend/.husky/_ does not exist — the dormancy/cross-context behavior.

The bug is systematic across worktrees on the same machine:

$ for w in affectionate-cray-18805d awesome-thompson-7c0a94 cool-solomon-082b06 cranky-mirzakhani-663c1b distracted-hamilton-3bbfec; do
    echo "--- $w ---"
    cat $GIT_COMMON_DIR/worktrees/$w/config.worktree
  done

--- affectionate-cray-18805d ---
[core]
    longpaths = true
    hooksPath = D:\\projects\\invoiceflow-AI\\frontend\\.husky\\_
--- awesome-thompson-7c0a94 ---
[core]
    longpaths = true
    hooksPath = D:\\projects\\invoiceflow-AI\\frontend\\.husky\\_
--- cool-solomon-082b06 ---
[core]
    longpaths = true
    hooksPath = D:\\projects\\invoiceflow-AI\\frontend\\.husky\\_
--- cranky-mirzakhani-663c1b ---
[core]
    longpaths = true
    hooksPath = D:\\projects\\invoiceflow-AI\\frontend\\.husky\\_
--- distracted-hamilton-3bbfec ---
[core]
    longpaths = true
    hooksPath = D:\\projects\\invoiceflow-AI\\frontend\\.husky\\_

Shared .git/config (untouched, correct):

[core]
    repositoryformatversion = 0
    ...
    hooksPath = frontend/.husky/_
[extensions]
    worktreeConfig = true

Relationship to #27474 / #30251

These earlier reports flagged that the harness wrote core.hooksPath into the shared $GIT_COMMON_DIR/config, which corrupted the main checkout. The harness now correctly scopes the write to per-worktree config.worktree — addressing the #27474 concern — but the value it writes is still derived from the main repo's directory rather than the worktree's. The end-user impact has moved from "main checkout broken" to "every Claude worktree silently has dormant or cross-context hooks." It is the same root cause (the path is computed from the wrong base directory) surfacing in a new file.

Impact

For any project using Husky, lefthook, pre-commit, or any repo-hook tool that installs a repo-relative core.hooksPath:

  • pre-commit, commit-msg, pre-push, post-merge, etc. do not run inside Claude-created worktrees.
  • Whatever those hooks were meant to enforce (typecheck, lint, secret scanning, schema-drift checks, build gates, conventional-commits, signing) is silently bypassed for every commit Claude makes.
  • No warning is surfaced to the user. The first signal is typically CI failing on something local hooks would have caught — or in worse cases, a bug landing because the main checkout's hooks pass against stale code in a different directory.

Suggested Fix

The simplest and least-invasive fix:

-# Currently in $GIT_COMMON_DIR/worktrees/<name>/config.worktree
-[core]
-    longpaths = true
-    hooksPath = <absolute path to main repo's hooks dir>
+# Proposed:
+[core]
+    longpaths = true
+# (omit core.hooksPath entirely — let it resolve from the shared config)

If the harness has a use case that genuinely requires per-worktree core.hooksPath, derive the absolute path from the worktree's own root (e.g., the path passed to git worktree add), not the main checkout. The longpaths = true setting is independently fine and can remain.

Claude Model

Not sure / Multiple models (harness bug, model-independent).

Is this a regression?

No, this never worked. The failure mode shifted from #27474 (writes corrupted the shared .git/config) to its current shape (writes a bad value into per-worktree config.worktree), but hooks have never resolved correctly inside Claude-created worktrees for repos using a repo-relative core.hooksPath.

Last Working Version

N/A — never worked.

Claude Code Version

2.1.131 (Claude Code)

Platform

Anthropic API

Operating System

Windows (build 10.0.26200, Windows 11 Pro).

Terminal/Shell

PowerShell (PS 5.1; Claude Code's Bash tool also reproduces from /usr/bin/bash via git-bash).

Additional Information

Reproduction repo layout (for context):

  • Main checkout: D:\projects\invoiceflow-AI\
  • Husky installed at: frontend/.husky/ with prepare script cd .. && husky frontend/.husky (per Husky 9's subdirectory pattern: https://typicode.github.io/husky/how-to.html)
  • Shared core.hooksPath (correct): frontend/.husky/_
  • Worktrees: D:\projects\invoiceflow-AI\.claude\worktrees\<name>\
  • Git version: 2.51.0.windows.1
  • Node version: v22.19.0

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

claude-code - ✅(Solved) Fix Claude Code agent harness writes broken per-worktree core.hooksPath [1 pull requests, 2 comments, 2 participants]