Repository navigation
fix: don't force-install hooks from the npm postinstall - #1510
Conversation
--force means two things: overwrite .old files, and proceed even if core.hooksPath is set. The postinstall wanted the first and was silently getting the second, so the guard added in evilmartians#1292 never fired for anyone installing through npm. It reaches further than the project being installed. npm runs prepare for git dependencies in a temp clone under the npm cache, which is itself a git repo, so the user's *global* core.hooksPath applies there. Installing an unrelated package can therefore rewrite hooks shared by every repository on the machine, without the user typing lefthook install. Dropping -f costs nothing here. cleanHook only consults force when a foreign hook and its .old backup both already exist; an existing lefthook hook is recognised and replaced without it, which is the case a postinstall actually hits on upgrade. That path is already covered by "with existing lefthook hooks" in internal/command/install_test.go, which passes force=false. Closes evilmartians#1487 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Confidence Score: 3/5This PR should not merge until the maintained npm-installer distribution is prevented from retaining the same automatic global-hooks overwrite path. Two postinstall variants now respect Files Needing Attention: packaging/registries/npm/lefthook/postinstall.js, packaging/registries/npm-installer/install.js, docs/usage/commands/install.md
|
| } | ||
|
|
||
| spawnSync(getExePath(), ["install", "-f"], { | ||
| // No -f here. --force also tells `install` to proceed when core.hooksPath is |
There was a problem hiding this comment.
Installer variant still bypasses guard
When a user with global core.hooksPath installs the maintained @evilmartians/lefthook-installer package, its automatic lifecycle script still runs lefthook install -f, causing unattended installation to overwrite hooks in the shared directory despite this safety fix. How this was verified: The published package's install lifecycle invokes the same CLI with -f, which makes the hooks-path check proceed instead of refusing.
| ::: callout info Note | ||
| NPM package `lefthook` installs the hooks in a postinstall script automatically. For projects not using NPM package run `lefthook install` after cloning the repo. | ||
|
|
||
| The postinstall script runs a plain `lefthook install`, so when `core.hooksPath` is set it stops and prints the same message as running the command by hand. Run `lefthook install --force` or `lefthook install --reset-hooks-path` once, deliberately, to resolve it. |
There was a problem hiding this comment.
Force is not persistent resolution
The new guidance says either command resolves the conflict when run once, but --force only bypasses core.hooksPath for the current invocation. The next plain postinstall stops again; only --reset-hooks-path persistently removes the conflicting setting.
| The postinstall script runs a plain `lefthook install`, so when `core.hooksPath` is set it stops and prints the same message as running the command by hand. Run `lefthook install --force` or `lefthook install --reset-hooks-path` once, deliberately, to resolve it. | |
| The postinstall script runs a plain `lefthook install`, so when `core.hooksPath` is set it stops and prints the same message as running the command by hand. Run `lefthook install --force` to override the check for one installation, or run `lefthook install --reset-hooks-path` once to remove the setting. |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Closes #1487
Context
--forcemeans two things at once βcmd/install.gospells it out: "overwrite .old files and proceed even if core.hooksPath is set". The npm postinstall runslefthook install -f, so it wants the first half and silently gets the second, and thecore.hooksPathguard added in #1292 never fires for anyone installing through npm.It reaches past the project being installed. npm runs
preparefor git dependencies in a temp clone under the npm cache; that clone is itself a git repo, so the user's globalcore.hooksPathapplies to it. Installing an unrelated package can rewrite hooks shared by every repository on the machine, without anyone typinglefthook install.Reproduced with a binary built from
master, in a repo whosecore.hooksPathholds a hand-writtenpre-commit:Changes
Drop
-ffrom both postinstall scripts, and note the resulting behavior in the install docs.Dropping it costs nothing here.
cleanHookonly consultsforcewhen a foreign hook and its.oldbackup both already exist β an existing lefthook hook is matched byisLefthookFileand removed without it, which is the case a postinstall actually hits on upgrade.internal/command/install_test.goalready pins that as"with existing lefthook hooks"withforceunset, and a second plainlefthook installagainst a real repo reportssync hooks: βοΈ(pre-commit)and creates no.old.The only behavior that changes is foreign-hook-plus-existing-
.old, which now stops with the existing error instead of overwriting the backup unasked.--forceis unchanged and still there for whoever means it.spawnSyncdoes not check the exit status, sonpm installstill succeeds; when the guard fires the user sees its message and instructions.Not addressed here, from the same issue: the generated wrapper embeds the install-time absolute binary path. That's a separate concern and I'd rather it got its own issue than rode along on this one.
Checklist
make lintpasses βgo vet ./...clean;golangci-lintnot run locally (Windows), CI will cover itmake testpasses β fullgo test ./...greenBranch note: based on the
2.1.11commit rather than currentmaster, because the token used here lacks theworkflowscope needed to push newer upstream commits that touch.github/workflows/. None of the three files touched differ between the two, so it applies tomasterunchanged β happy to rebase.AI disclosure: this change was prepared with the assistance of Claude Code. I reviewed the change and the verification before opening, and I stand behind them as my own.