Husky & lint-staged (Pre-commit Hooks)
Run ESLint and Prettier automatically on staged files before each commit to catch issues early.
Search across all documentation pages
Run ESLint and Prettier automatically on staged files before each commit to catch issues early.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Quick-reference recipe card - copy-paste ready.
# Install husky and lint-staged
npm install --save-dev husky lint-staged
# Initialize husky
npx husky init
# The init command creates .husky/pre-commit
# Edit it to run lint-staged
echo "npx lint-staged" > .husky/pre-commitWhen to reach for this: Any team project where you want to guarantee that committed code passes linting and formatting checks.
# .husky/pre-commit
npx lint-staged// package.json
{
"scripts": {
"lint": "next lint",
"format": "prettier --write .",
"prepare": "husky"
},
"lint-staged": {
"*.{ts,tsx}": [
"eslint --fix --no-warn-ignored",
"prettier --write"
],
"*.{css,json,md}": [
"prettier --write"
]
}
}What this demonstrates:
git add)prepare script ensures husky installs automatically after npm install.husky/pre-commit) to run commands before each commitgit diff --staged and passes them to the configured commandsprepare script runs after npm install, ensuring hooks are set up for every developerWith Biome instead of ESLint + Prettier:
{
"lint-staged": {
"*.{ts,tsx,js,jsx,json,css}": [
"biome check --write --no-errors-on-unmatched"
]
}
}With type-checking (slower but thorough):
{
"lint-staged": {
"*.{ts,tsx}": [
"eslint --fix --no-warn-ignored",
"prettier --write"
]
}
}# .husky/pre-commit
npx lint-staged
npx tsc --noEmitNote: tsc --noEmit checks the entire project, not just staged files, because TypeScript needs full project context.
Commit message linting with commitlint:
npm install --save-dev @commitlint/cli @commitlint/config-conventional
echo "npx commitlint --edit \$1" > .husky/commit-msg// commitlint.config.js
export default { extends: ["@commitlint/config-conventional"] };Skipping hooks (escape hatch):
# When you really need to bypass (debugging, WIP commits)
git commit --no-verify -m "WIP: work in progress"// lint-staged passes file paths to ESLint, which works with
// TypeScript files seamlessly as long as @typescript-eslint
// is configured.
// Note: ESLint --fix can auto-fix some TypeScript issues:
// - Remove unused imports
// - Add `type` keyword to type-only imports
// - Fix consistent-type-imports violationsThings that will bite you. Each gotcha includes what goes wrong, why it happens, and the fix.
Hooks not running after clone - Git hooks are not committed to the repo; they live in .git/hooks/. New developers need to run npm install (which triggers prepare). Fix: Ensure "prepare": "husky" is in your package.json scripts.
ESLint warnings on ignored files - When lint-staged passes file paths to ESLint, files matching your ESLint ignores trigger warnings. Fix: Add --no-warn-ignored flag to the ESLint command in lint-staged config.
Partial staging issues - If you stage only part of a file (git add -p), lint-staged operates on the full file, which may include unstaged changes. Fix: Be aware of this limitation. For critical cases, commit the full file.
Slow pre-commit hooks - Running type-checking (tsc) on every commit can take 10 or more seconds on large projects. Fix: Run tsc only in CI. Keep pre-commit hooks fast by limiting them to ESLint and Prettier on staged files.
CI does not run hooks - Git hooks are local only. CI environments do not execute pre-commit hooks. Fix: Always run lint and format checks in CI as a safety net. See Linting in CI/CD.
Other ways to solve the same problem - and when each is the better choice.
| Alternative | Use When | Don't Use When |
|---|---|---|
lefthook | You want faster hooks with parallel execution, written in Go | Husky + lint-staged works fine for your project |
| CI-only linting | You do not want to slow down local commits | You want instant feedback before pushing |
nano-staged | You want a smaller, faster alternative to lint-staged | You need advanced lint-staged features like custom resolvers |
| VS Code format-on-save | Solo developer, no CI | Team projects where consistency must be enforced |
npm install.npx husky init.git diff --staged (files you have git add-ed).git commit --no-verify -m "WIP: work in progress"Use this sparingly for debugging or WIP commits. CI will still catch issues.
.git/hooks/, which is not committed to the repo.npm install, which triggers the prepare script."prepare": "husky" is in your package.json scripts.# .husky/pre-commit
npx lint-staged
npx tsc --noEmittsc --noEmit checks the entire project, not just staged files.tsc only in CI to keep hooks fast.{
"lint-staged": {
"*.{ts,tsx,js,jsx,json,css}": [
"biome check --write --no-errors-on-unmatched"
]
}
}npm install --save-dev @commitlint/cli @commitlint/config-conventional
echo "npx commitlint --edit \$1" > .husky/commit-msgThis enforces conventional commit messages like feat:, fix:, docs:.
git add files after ESLint or Prettier auto-fix.--no-warn-ignored to the ESLint command in your lint-staged config.type keyword to type-only imports.consistent-type-imports violations.import/order rules.Reviewed by Chris St. John·Last updated Jul 16, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥