bug: /worktree can silently write core.hooksPath into the MAIN repo's shared .git/config, disabling global hooks
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-codeVS Code extension2.1.197-darwin-arm64 - OS: macOS (Darwin 25.5.0, Apple Silicon)
- Git: 2.50.1 (Apple Git-155)
- Repo setup: global
core.hooksPathset in~/.gitconfig(pointing at a shared hooks directory used across ~50 repos, containing apost-commithook), 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 commitsucceeds 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
/worktreecreate for the same repo, even after manually runninggit config --local --unset core.hooksPathto 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/worktreeusage on the same repo.
Reproduction
- Set a global hook:
git config --global core.hooksPath ~/.git-hooks, with apost-commitscript there that does something observable (e.g. appends to a log file). - In a repo with no local
core.hooksPathoverride, confirm hooks fire:git commit --allow-empty -m testshould trigger the globalpost-commit. - Run
/worktree(or the worktree-create tool) to create a new worktree for that repo. - Check the main repo's
.git/config— a[core] hooksPath = <repo>/.git/hooksentry has appeared. git commit --allow-empty -m test2in the main repo (not the worktree) — the globalpost-commithook no longer fires, because the newly-written local override now points at the repo's own (empty, sample-only).git/hooksdirectory.
Suggested fix
- Either pass
--worktreeexplicitly on thegit config core.hooksPath <value>call (requiresextensions.worktreeConfigto 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 togit configfrom within the worktree's cwd, or - Skip the propagation entirely when the main repo's
core.hooksPathis already absolute (which should be the common case for anyone using a shared/global-style hooks directory) — the currentif (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 whathooksPathValueactually 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.