Pre-commit hooks are the last line of defense before code leaves your machine. They catch the fast-and-cheap problems — formatting drift, obvious lint errors, accidental secret commits — so CI does not have to. In 2026, the tooling is mature and setup takes under ten minutes. Here is the canonical approach.
What changed in 2026
- Husky v10 dropped the
prepare script pattern and uses husky init to write a native shell script, reducing setup to two commands.
- lint-staged 16 added parallel task execution — multiple formatters and linters run concurrently on staged file groups.
gitleaks v9 ships a pre-built binary for all platforms and is the standard secrets scanner for git hooks.
- Node projects with pnpm can use
pnpm dlx husky init without a global install.
Install Husky and lint-staged
npm install --save-dev husky lint-staged
npx husky init
husky init creates .husky/pre-commit and adds a prepare script. Replace the default content:
# .husky/pre-commit
npx lint-staged
Configure lint-staged
In package.json:
{
"lint-staged": {
"*.{js,ts,jsx,tsx}": [
"eslint --fix --max-warnings=0",
"prettier --write"
],
"*.{json,css,md,yaml}": "prettier --write",
"*.{js,ts}": "bash -c 'tsc --noEmit'"
}
}
Alternatively, create lint-staged.config.mjs for more complex logic.
Add a secrets scanner
Install gitleaks (pick the method for your OS):
# macOS
brew install gitleaks
# Linux / CI
curl -sSfL https://raw.githubusercontent.com/gitleaks/gitleaks/main/scripts/install.sh | sh -s -- -b /usr/local/bin
Add a second hook file:
# .husky/pre-commit (append)
gitleaks protect --staged --redact
gitleaks protect --staged scans only staged diff content and exits non-zero if secrets are detected.
Hook execution order and types
| Hook |
When it runs |
Typical use |
pre-commit |
Before commit message entry |
Lint, format, secrets scan |
commit-msg |
After message written |
Validate commit message format |
pre-push |
Before git push |
Type-check, heavier checks |
post-checkout |
After branch switch |
Install deps if lockfile changed |
For the commit-msg hook, enforce Conventional Commits:
# .husky/commit-msg
npx --no -- commitlint --edit "$1"
Monorepo setup
In a monorepo with workspaces, Husky hooks live at the repo root. Configure lint-staged to scope commands to the right package:
{
"lint-staged": {
"packages/api/**/*.ts": "eslint --fix",
"packages/web/**/*.{ts,tsx}": ["eslint --fix", "prettier --write"],
"**/*.{json,md}": "prettier --write"
}
}
How to pick what goes in a hook
- Under 3 seconds total? Fine for pre-commit.
- Fails rarely and only on real problems? Good — hooks that always pass are useless.
- Requires a running server or DB? Move it to CI.
- Full type-check on a large project? Put it in
pre-push, not pre-commit.
Common mistakes
Hooks not executable. Git hooks must have execute permission. Husky handles this, but if you manually create a hook file run chmod +x .husky/pre-commit.
Bypassing with --no-verify. Some teams do this so often hooks become irrelevant. Keep hooks fast — under 3 seconds — so there is no incentive to skip.
Not committing the .husky/ directory. Husky hooks live in .husky/ which must be committed. .git/hooks/ is per-clone and never shared.
Running all files instead of staged. Without lint-staged, eslint . on a large repo takes 30+ seconds. Always scope to staged files.
What to skip
- Full test suite in pre-commit. Unit tests belong in CI where they can run in parallel with caching. In a pre-commit hook they add 30–120 seconds and will be skipped.
- Build step in pre-commit. Build in CI; hook should only validate source.
- Custom shell scripts for formatting. Use lint-staged instead of hand-rolled
for loops over staged files.
FAQ
How do I skip the hook for one commit?
git commit --no-verify -m "wip". Use sparingly; CI will still catch problems.
Does Husky work with pnpm and Yarn Berry?
Yes. Replace npx with pnpm dlx or yarn dlx in hook files, and run pnpm dlx husky init during setup.
Can I share hooks across a team?
Yes — commit .husky/ to the repository. Every developer gets the hooks after npm install runs the prepare script.
What if a developer does not have a tool installed?
Guard with a check: command -v gitleaks &>/dev/null && gitleaks protect --staged. The hook skips gracefully if the binary is missing; document the install step in the README.
Where to go next