Skip to content

chore: replace husky and lint-staged with a Git hook - #165

Merged
cawalch merged 1 commit into
mainfrom
chore/replace-husky-with-git-hooks
Nov 1, 2026
Merged

cawalch merged 1 commit into
mainfrom
chore/replace-husky-with-git-hooks

Conversation

@cawalch

@cawalch cawalch commented Nov 1, 2026 •

Copy link
Copy Markdown
Collaborator

Remove husky and lint-staged — neither has ever run in this repository — and replace them with a checked-in Git hook that uses Biome's own --staged mode. No hook framework and no npx in the commit path.

Is .githooks idiomatic?

core.hooksPath with a version-controlled directory is Git's recommended mechanism (Git 2.9+) and is documented by Biome's own shell-script recipe. It is still a minority pattern: roughly 5,000 indexed public repositories configure it from package.json, against about 285,000 that use "prepare": "husky". So: legitimate, but not the default expectation — the trade-offs are called out below.

Why not keep the existing tools: there is no .husky/ directory and no prepare script, so both packages were dead weight. Husky's last release was v9.1.7 in November 2024 (only documentation commits since), and lint-staged@17 requires Node ≥22.22.1 while this package still declares >=20.19.0.

Why not swap in Lefthook (Biome's first recommendation): it is fast and actively maintained, but adds a Go binary plus a postinstall script that needs npm 12 install-script approval. For one Biome command, the native route is smaller and has no supply-chain surface.

Changes

• Remove husky, lint-staged, and .lintstagedrc.json (8 packages leave the tree).
• Add .githooks/pre-commit: format, organize imports, lint, and apply safe fixes to staged files only, then re-stage the result. It calls ./node_modules/.bin/biome rather than npx.
• Refuse to rewrite the index when staged files also have unstaged changes — every mixed state (MM, AM, RM, …), not just MM; git update-index --again would otherwise silently include those edits.
• Add scripts/install-hooks.js with prepare and hooks:install scripts. It reads core.hooksPath first and only writes when it is unset or already .githooks, so a developer's own hooks directory is never overwritten. Outside a Git checkout it exits silently.
• Migrate linter.recommended to preset (deprecated in Biome 2.6), which was printing a warning on every lint run.

Concerns and trade-offs

  • Auto-activation: prepare points Git at the hook after npm install, so checked-in code runs on the next commit. This is the same trust model as Husky. git commit --no-verify bypasses it, and an existing core.hooksPath is left untouched.
  • Windows: the hook is POSIX sh; Git for Windows provides one, but there is no Windows CI job to prove it.
  • Redundancy: CI already runs lint:ci and typecheck; the hook is a convenience, not the enforcement.
  • Opt-out: delete the prepare entry and keep npm run hooks:install explicit if automatic installation is unwanted.

Verification

  • Staged an unformatted file: the hook rewrote it (export const hookTest = { a: 1, b: 2 };) and re-staged the fix; git hook run pre-commit resolves via core.hooksPath.
  • Staged-then-edited a file (AM) and modified a tracked file after staging (MM): both refused with "Some staged files have unstaged changes".
  • Set core.hooksPath to .custom-hooks, ran the installer: value preserved. Unset it and ran again: set to .githooks.
  • npm run install-hooks outside a Git checkout: exits 0.
  • npm run lint:ci, npm run typecheck, npm run build, npm test — 1,380 tests passed; npm audit — 0 vulnerabilities.
  • npm pack --dry-run — 29 files, unchanged: files is dist/, so the hook and installer are not published.

CI (22.x/24.x/26.x), OSV, and verify-lockfile run on this pull request.

Both packages were installed but never wired: there is no `.husky/` directory and no `prepare` script, so no hook ever ran. Replace them with a checked-in hook that uses Biome's own `--staged` mode.

• Remove `husky`, `lint-staged`, and `.lintstagedrc.json`.
• Add `.githooks/pre-commit`, which formats and lints staged files with the local Biome binary and refuses to rewrite the index when staged files also have unstaged changes (`MM`, `AM`, and the other mixed states).
• Add `scripts/install-hooks.js` plus `prepare` and `hooks:install` scripts. It points `core.hooksPath` at `.githooks` only when unset or already pointing there, so a developer's own hooks directory is never overwritten, and exits silently outside a Git checkout.
• Migrate `linter.recommended` to `preset` (deprecated in Biome 2.6) so hook and CI output stay clean.
@cawalch
cawalch force-pushed the chore/replace-husky-with-git-hooks branch from 79861cf to a4ebb3a Compare November 1, 2026 16:52
@cawalch
cawalch merged commit 99792f7 into main Nov 1, 2026
9 checks passed
@cawalch
cawalch deleted the chore/replace-husky-with-git-hooks branch November 1, 2026 16:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant