Summary
lefthook install generates a hook shim in .git/hooks/<name> that contains a hardcoded absolute path to the specific node_modules/ directory that existed at install time. Because .git/hooks/ is shared across all git worktrees of a repository, the shim installed by one worktree leaks into all other worktrees — and if the original worktree is ever deleted (or its pnpm content-addressed store changes), the shim silently falls through to a no-op in every other worktree.
Environment
- lefthook
v2.1.5 (installed via pnpm, lefthook-darwin-arm64@2.1.5)
- pnpm
10.24.0
- macOS, arm64
- Repo uses git worktrees (
git worktree add ...)
Repro
- In a repo with lefthook wired up via
\"prepare\": \"lefthook install --force\" and installed as an npm dev dependency:
git worktree add ../wt-a -b feature/a
cd ../wt-a && pnpm install # runs `lefthook install --force` → writes hooks into shared .git/hooks/
- Create a second worktree later:
git worktree add ../wt-b -b feature/b
cd ../wt-b # no pnpm install yet — this is the critical state
- Delete or invalidate the first worktree's
node_modules/ (e.g. rm -rf ../wt-a, or prune the pnpm store, or simply use a worktree created before pnpm install has run there):
- Commit in the second worktree:
cd ../wt-b
git commit -am 'test'
- Observe stderr:
Can't find lefthook in PATH — commit succeeds with exit 0, and pre-commit jobs are silently skipped.
Root cause
The generated shim at .git/hooks/pre-commit contains (verbatim, from our repo):
call_lefthook()
{
if test -n \"\$LEFTHOOK_BIN\"
then
\"\$LEFTHOOK_BIN\" \"\$@\"
elif lefthook -h >/dev/null 2>&1
then
lefthook \"\$@\"
elif /Users/<user>/…/worktrees/wt-a/node_modules/.pnpm/lefthook-darwin-arm64@2.1.5/node_modules/lefthook-darwin-arm64/bin/lefthook -h >/dev/null 2>&1
then
/Users/<user>/…/worktrees/wt-a/node_modules/.pnpm/lefthook-darwin-arm64@2.1.5/node_modules/lefthook-darwin-arm64/bin/lefthook \"\$@\"
else
dir=\"\$(git rev-parse --show-toplevel)\"
# … relative \$dir/node_modules/<variants> fallbacks …
fi
}
Two problems compound:
- The third branch is a hardcoded absolute path captured at
lefthook install time (specifically, the node_modules/.pnpm/... path of whichever worktree happened to run the install). This path is brittle by construction — it's tied to a single working tree, a single pnpm hash, and a single OS/arch.
.git/hooks/ is shared across all worktrees, so this install-time-captured absolute path is inherited by every other worktree of the same repository — even worktrees that have their own perfectly valid node_modules/lefthook-<os>-<arch>/bin/lefthook, which the shim would find via the existing relative fallback in the else branch if it were reached.
The existing \$dir/node_modules/... fallback (using git rev-parse --show-toplevel) is already correct for multi-worktree setups. The hardcoded absolute branch just shadows it in a broken way.
Proposed fix
Remove the absolute-path branch from the generated shim. The \$dir/node_modules/lefthook-\${osArch}-\${cpuArch}/bin/lefthook fallback (and the other \$dir-relative fallbacks) already cover the same case correctly, per-worktree, without any install-time snapshotting.
This is a codegen-only change — the shim logic for single-checkout repos is unaffected (the relative fallback finds the same binary). It eliminates the multi-worktree failure mode entirely.
Happy to send a PR if you're open to this direction.
Related but out of scope for this issue
When the shim does fall through every branch, the terminal echo \"Can't find lefthook in PATH\" exits 0 rather than 1, which is why this bug was invisible for a long time (unformatted commits silently landed and only failed at CI). I understand that's a deliberate posture to not block contributors without lefthook installed, so I'm not proposing to change it here — just flagging it as the reason the absolute-path bug above is especially dangerous in practice.
related: #1384
Summary
lefthook installgenerates a hook shim in.git/hooks/<name>that contains a hardcoded absolute path to the specificnode_modules/directory that existed at install time. Because.git/hooks/is shared across all git worktrees of a repository, the shim installed by one worktree leaks into all other worktrees — and if the original worktree is ever deleted (or its pnpm content-addressed store changes), the shim silently falls through to a no-op in every other worktree.Environment
v2.1.5(installed via pnpm,lefthook-darwin-arm64@2.1.5)10.24.0git worktree add ...)Repro
\"prepare\": \"lefthook install --force\"and installed as an npm dev dependency:node_modules/(e.g.rm -rf ../wt-a, or prune the pnpm store, or simply use a worktree created beforepnpm installhas run there):Can't find lefthook in PATH— commit succeeds with exit 0, and pre-commit jobs are silently skipped.Root cause
The generated shim at
.git/hooks/pre-commitcontains (verbatim, from our repo):Two problems compound:
lefthook installtime (specifically, thenode_modules/.pnpm/...path of whichever worktree happened to run the install). This path is brittle by construction — it's tied to a single working tree, a single pnpm hash, and a single OS/arch..git/hooks/is shared across all worktrees, so this install-time-captured absolute path is inherited by every other worktree of the same repository — even worktrees that have their own perfectly validnode_modules/lefthook-<os>-<arch>/bin/lefthook, which the shim would find via the existing relative fallback in theelsebranch if it were reached.The existing
\$dir/node_modules/...fallback (usinggit rev-parse --show-toplevel) is already correct for multi-worktree setups. The hardcoded absolute branch just shadows it in a broken way.Proposed fix
Remove the absolute-path branch from the generated shim. The
\$dir/node_modules/lefthook-\${osArch}-\${cpuArch}/bin/lefthookfallback (and the other\$dir-relative fallbacks) already cover the same case correctly, per-worktree, without any install-time snapshotting.This is a codegen-only change — the shim logic for single-checkout repos is unaffected (the relative fallback finds the same binary). It eliminates the multi-worktree failure mode entirely.
Happy to send a PR if you're open to this direction.
Related but out of scope for this issue
When the shim does fall through every branch, the terminal
echo \"Can't find lefthook in PATH\"exits 0 rather than 1, which is why this bug was invisible for a long time (unformatted commits silently landed and only failed at CI). I understand that's a deliberate posture to not block contributors without lefthook installed, so I'm not proposing to change it here — just flagging it as the reason the absolute-path bug above is especially dangerous in practice.related: #1384