bug: /worktree can silently write core.hooksPath into the MAIN repo's shared .git/config, disabling global hooks

Open 💬 0 comments Opened Jul 1, 2026 by illumedigitalstudio

Summary

claude's /worktree feature can silently write core.hooksPath into the main repository's shared .git/config instead of a worktree-scoped location, permanently disabling any git hooks configured via a global core.hooksPath (e.g. ~/.git-hooks) for the main checkout — not just the worktree.

Environment

  • Claude Code version: bundled in anthropic.claude-code VS Code extension 2.1.197-darwin-arm64
  • OS: macOS (Darwin 25.5.0, Apple Silicon)
  • Git: 2.50.1 (Apple Git-155)
  • Repo setup: global core.hooksPath set in ~/.gitconfig (pointing at a shared hooks directory used across ~50 repos, containing a post-commit hook), no local override on the affected repos before the bug fires.

What happens

When a new worktree is created (via /worktree or the underlying git_worktree_create tool), the CLI runs logic equivalent to (reconstructed from the bundled binary, function names are minified):

// roughly:
let mainGitDir = await resolveCommonGitDir(mainRepoPath);
let hooksPathValue = mainGitDir ? await readRawConfigValue(mainGitDir, "core", null, "hooksPath") : null;
if (hooksPathValue) {
    let absolute = isAbsolute(hooksPathValue) ? hooksPathValue : resolve(mainRepoPath, hooksPathValue);
    if (hooksPathValue !== absolute) {
        // cwd is the NEW WORKTREE path, not the main repo
        await run("git", ["config", "core.hooksPath", absolute], { cwd: newWorktreePath });
        // logs: "Configured worktree to use hooks from main repository: <absolute>"
    }
}

The intent is clearly to propagate a relative core.hooksPath (e.g. .husky) from the main repo into the worktree as an absolute path, since a relative hooksPath resolved from the worktree's cwd would point to the wrong place.

The problem: the git config core.hooksPath <value> call is run with cwd set to the worktree directory, but with no --worktree scope flag and no extensions.worktreeConfig enabled (git's default). Per git's default config-writing behavior, a plain git config <key> <value> invoked from any linked worktree writes to the shared $GIT_COMMON_DIR/config — i.e. the main repository's own .git/config — not a worktree-private file. (core.hooksPath is also not one of the small set of keys git auto-scopes to config.worktree even when extensions.worktreeConfig is enabled, so enabling that extension does not prevent this.)

Net effect: every time a new worktree is created for a repo where the main repo's config contains any core.hooksPath value (including, apparently, cases where reading it nets a value that differs from its already-absolute form — we did not fully pin down why this fires even when we expected no local override to be present at read time, possibly due to the raw-file-parse read path picking up something we haven't isolated), the main repo's shared .git/config gets an explicit [core] hooksPath = <main-repo>/.git/hooks written into it. This shadows any global core.hooksPath (e.g. ~/.git-hooks, a common pattern for sharing hooks across many repos) for the main checkout, silently disabling every hook configured there — in our case, an auto-push-on-commit hook, so commits made afterward on the main checkout stop reaching origin with no error surfaced anywhere in the git commit output.

Impact

  • Silent: git commit succeeds normally; nothing in Claude Code's or git's own output indicates hooks are now disabled for the main repo.
  • Recurring: happens again on every subsequent /worktree create for the same repo, even after manually running git config --local --unset core.hooksPath to fix it.
  • Affects any user relying on a global core.hooksPath (e.g. team-wide shared hooks, husky-style setups pointed at a non-default location) combined with /worktree usage on the same repo.

Reproduction

  1. Set a global hook: git config --global core.hooksPath ~/.git-hooks, with a post-commit script there that does something observable (e.g. appends to a log file).
  2. In a repo with no local core.hooksPath override, confirm hooks fire: git commit --allow-empty -m test should trigger the global post-commit.
  3. Run /worktree (or the worktree-create tool) to create a new worktree for that repo.
  4. Check the main repo's .git/config — a [core] hooksPath = <repo>/.git/hooks entry has appeared.
  5. git commit --allow-empty -m test2 in the main repo (not the worktree) — the global post-commit hook no longer fires, because the newly-written local override now points at the repo's own (empty, sample-only) .git/hooks directory.

Suggested fix

  • Either pass --worktree explicitly on the git config core.hooksPath <value> call (requires extensions.worktreeConfig to be enabled first, and would still need a check that hooksPath isn't one of the keys git refuses to auto-scope), or
  • Write the value directly into the worktree's own private config file (.git/worktrees/<name>/config) via a raw file write rather than shelling out to git config from within the worktree's cwd, or
  • Skip the propagation entirely when the main repo's core.hooksPath is already absolute (which should be the common case for anyone using a shared/global-style hooks directory) — the current if (hooksPathValue !== absolute) guard appears intended to do exactly this, but something upstream of it is causing it to fire when we did not expect a local override to exist yet; worth auditing what hooksPathValue actually resolves to when there is no explicit local config, since global-config inheritance should not need worktree-side propagation at all.

Workaround we shipped in the meantime

Since we can't patch the shipped binary, we built a self-healing watchdog on our end: a launchd job with WatchPaths on the affected repos' .git/config (near-instant reaction) plus a 15-minute interval backstop, which detects the override, unsets it, pushes any commits that got stranded local-only while it was shadowed, and posts a Slack alert either way. Happy to share the script if useful for a regression test on your end.

View original on GitHub ↗