husky vs lefthook vs lint-staged: Git Hooks in Node.js (2026)
TL;DR
husky is the most popular Git hooks manager for Node.js — simple setup, integrates with npm scripts, and runs hooks defined as shell scripts. lefthook is the fast multi-language alternative — written in Go, runs hooks in parallel, ~10x faster than husky for large projects. lint-staged is not a hook runner but a companion tool — it runs linters and formatters only on files staged for commit (not your entire codebase). In 2026: use husky + lint-staged together for the industry-standard pre-commit setup, or lefthook as an all-in-one faster alternative.
Key Takeaways
- husky: ~5M weekly downloads — de facto Git hooks standard for Node.js, simple
.husky/directory - lefthook: ~400K weekly downloads — Go-based, parallel execution, monorepo-aware, no Node.js required
- lint-staged: ~8M weekly downloads — runs linters only on staged files, dramatically faster than full codebase lint
- husky + lint-staged is the industry standard — used by React, Next.js, Vite, and thousands of OSS projects
- lefthook supports running commands in parallel within a hook — pre-commit can lint + type-check simultaneously
- lint-staged works with any hook runner — husky, lefthook, or plain Git hooks
The Problem Git Hooks Solve
Without Git hooks:
1. Developer commits broken code
2. CI catches the issue 5 minutes later
3. Developer context-switches back to fix it
→ Slow feedback loop, inconsistent code quality
With pre-commit hooks:
1. Developer runs git commit
2. Hook: ESLint runs, finds error → commit rejected
3. Developer fixes immediately, recommits
→ Fast feedback, consistent quality, no CI noise
husky
husky — Git hooks for Node.js:
Setup
npm install -D husky
# Initialize husky (creates .husky/ directory, updates package.json):
npx husky init
# Creates:
# .husky/pre-commit (sample hook)
# package.json: "prepare": "husky" (installs hooks on npm install)
Hook files
# .husky/pre-commit
npm run lint
npm run type-check
# .husky/commit-msg
# Validate commit message format (requires commitlint):
npx --no -- commitlint --edit $1
# .husky/pre-push
npm run test
# Hook files are just shell scripts — make executable:
chmod +x .husky/pre-commit
# Test a hook manually:
./.husky/pre-commit
With lint-staged (recommended)
# Install both:
npm install -D husky lint-staged
# .husky/pre-commit:
npx lint-staged
# package.json (lint-staged config):
{
"lint-staged": {
"*.{ts,tsx}": ["eslint --fix", "prettier --write"],
"*.{js,jsx}": ["eslint --fix"],
"*.{css,md,json}": ["prettier --write"]
}
}
commitlint integration
npm install -D @commitlint/cli @commitlint/config-conventional
# .husky/commit-msg:
npx --no -- commitlint --edit $1
# commitlint.config.js:
export default {
extends: ["@commitlint/config-conventional"],
rules: {
"type-enum": [2, "always", [
"feat", "fix", "docs", "style", "refactor",
"perf", "test", "chore", "ci", "revert"
]],
"subject-max-length": [2, "always", 100],
},
}
# Valid: "feat: add user authentication"
# Invalid: "add stuff" → hook rejects, shows error
CI environments
# husky skips installation in CI automatically (HUSKY=0 or CI env var):
# Or disable manually:
HUSKY=0 npm ci # Skip husky in CI pipeline
lint-staged
lint-staged — run linters on staged Git files:
Why lint-staged matters
Without lint-staged:
Pre-commit: eslint src/ → lints all 500 files → ~30 seconds
You changed: 2 files
With lint-staged:
Pre-commit: eslint <staged> → lints 2 changed files → ~0.5 seconds
Much faster, much more practical
Configuration
// package.json
{
"lint-staged": {
// Glob patterns → commands to run on matching staged files:
"*.{ts,tsx}": [
"eslint --fix --max-warnings 0",
"prettier --write"
],
"*.{js,mjs,cjs}": [
"eslint --fix"
],
"*.{json,yaml,yml}": [
"prettier --write"
],
"*.md": [
"prettier --write --prose-wrap always"
]
}
}
// Or as lint-staged.config.ts (TypeScript config):
import type { Config } from "lint-staged"
const config: Config = {
"*.{ts,tsx}": (files) => {
// files = array of staged file paths
// Return command(s) as string or string[]:
return [
`eslint --fix ${files.join(" ")}`,
`prettier --write ${files.join(" ")}`,
]
},
"*.{css,scss}": ["stylelint --fix", "prettier --write"],
}
export default config
Advanced patterns
{
"lint-staged": {
// Run type-check on ALL TypeScript files when any .ts changes:
"*.{ts,tsx}": [
"eslint --fix",
"prettier --write",
// Type-check the whole project (not just staged files):
// bash -c "tsc --noEmit"
],
// Run tests for changed files:
"src/**/*.test.{ts,tsx}": "vitest run",
// Validate package.json:
"package.json": "node -e \"JSON.parse(require('fs').readFileSync('package.json'))\""
}
}
lefthook
lefthook — fast polyglot Git hooks manager:
Setup
npm install -D @lintrunner/lefthook
# Or (recommended — standalone binary):
npx lefthook install
# Creates: lefthook.yml
lefthook.yml
# lefthook.yml
pre-commit:
parallel: true # Run all commands in parallel!
commands:
lint:
glob: "*.{ts,tsx,js}"
run: npx eslint --fix {staged_files}
stage_fixed: true # Re-stage files after auto-fix
format:
glob: "*.{ts,tsx,js,json,css,md}"
run: npx prettier --write {staged_files}
stage_fixed: true
type-check:
run: npx tsc --noEmit
commit-msg:
commands:
commitlint:
run: npx commitlint --edit {1}
pre-push:
commands:
tests:
run: npm test
Parallel execution (lefthook's killer feature)
# husky runs sequentially:
# eslint → 5s
# prettier → 2s
# tsc → 8s
# Total: 15s
# lefthook with parallel: true
# eslint ┐
# prettier ┤ all at once → 8s (longest task)
# tsc ┘
Monorepo support
# lefthook.yml (root)
pre-commit:
commands:
# Only run in changed packages:
lint-api:
root: "packages/api/"
glob: "*.ts"
run: npm run lint
lint-ui:
root: "packages/ui/"
glob: "*.{ts,tsx}"
run: npm run lint
# Root-level check:
type-check-all:
run: npm run type-check -w
Skip hooks
# Skip all hooks for a commit (emergency):
git commit --no-verify -m "emergency fix"
# Skip specific lefthook commands:
LEFTHOOK_EXCLUDE=type-check git commit -m "wip"
# Disable lefthook in CI:
LEFTHOOK=0 git push
Feature Comparison
| Feature | husky | lefthook | lint-staged |
|---|---|---|---|
| Primary role | Hook runner | Hook runner | Staged-file filter |
| Language | Node.js | Go (binary) | Node.js |
| Parallel hooks | ❌ | ✅ | N/A |
| Lint only staged files | ❌ | ✅ ({staged_files}) | ✅ (core feature) |
| Monorepo support | Manual | ✅ First-class | ✅ |
| Config format | Shell scripts | YAML | JSON / JS |
| Auto re-stage fixed files | ❌ | ✅ (stage_fixed: true) | ✅ |
| Non-Node.js projects | ❌ | ✅ | ❌ |
| Weekly downloads | ~5M | ~400K | ~8M |
Recommended Setups
Standard Node.js project (husky + lint-staged)
// package.json
{
"scripts": {
"prepare": "husky",
"lint": "eslint src/",
"type-check": "tsc --noEmit"
},
"lint-staged": {
"*.{ts,tsx,js,jsx}": ["eslint --fix", "prettier --write"],
"*.{json,md,css,yml}": ["prettier --write"]
},
"devDependencies": {
"husky": "^9.0.0",
"lint-staged": "^15.0.0",
"eslint": "^9.0.0",
"prettier": "^3.0.0"
}
}
# .husky/pre-commit
npx lint-staged
# .husky/commit-msg
npx --no -- commitlint --edit $1
Monorepo or performance-sensitive (lefthook)
# lefthook.yml
pre-commit:
parallel: true
commands:
eslint:
glob: "*.{ts,tsx,js}"
run: npx eslint --fix {staged_files}
stage_fixed: true
prettier:
glob: "*.{ts,tsx,js,json,css,md}"
run: npx prettier --write {staged_files}
stage_fixed: true
typecheck:
run: npx tsc --noEmit --skipLibCheck
commit-msg:
commands:
commitlint:
run: npx commitlint --edit {1}
When to Use Each
Choose husky + lint-staged if:
- Node.js project — the industry standard, well-documented everywhere
- Need maximum compatibility with tutorials, docs, and community help
- Simple hook needs (lint on commit, test on push)
Choose lefthook if:
- Large codebase where sequential hooks are too slow
- Monorepo with per-package hook configuration
- Non-Node.js projects (Go, Ruby, Python) that need Git hooks
- Want auto re-staging of auto-fixed files without extra config
Use lint-staged with either runner:
- Always use lint-staged to scope linting to staged files only
- Dramatically reduces pre-commit time from 30s → 1-2s
- Works with husky, lefthook, or plain
.git/hooks/
Methodology
Download data from npm registry (weekly average, February 2026). Feature comparison based on husky v9.x, lefthook v1.x, and lint-staged v15.x.
Compare developer tooling and Git workflow packages on PkgPulse →