React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

huskylint-stagedpre-commitgit-hooksautomation

Husky & lint-staged (Pre-commit Hooks)

Run ESLint and Prettier automatically on staged files before each commit to catch issues early.

Recipe

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-commit

When to reach for this: Any team project where you want to guarantee that committed code passes linting and formatting checks.

Working Example

# .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:

  • Husky hooks into Git's pre-commit event
  • lint-staged runs ESLint and Prettier only on files that are staged (git add)
  • TypeScript files get both linted and formatted; other files get formatted only
  • The prepare script ensures husky installs automatically after npm install

Deep Dive

How It Works

  • Husky uses Git hooks (.husky/pre-commit) to run commands before each commit
  • lint-staged collects files from git diff --staged and passes them to the configured commands
  • Commands run in sequence per glob pattern — if ESLint fails, Prettier does not run, and the commit is blocked
  • Fixed files are automatically re-staged so the commit includes the fixes
  • The prepare script runs after npm install, ensuring hooks are set up for every developer

Variations

With 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 --noEmit

Note: 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"

TypeScript Notes

// 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 violations

Gotchas

Things 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.

Alternatives

Other ways to solve the same problem — and when each is the better choice.

AlternativeUse WhenDon't Use When
lefthookYou want faster hooks with parallel execution, written in GoHusky + lint-staged works fine for your project
CI-only lintingYou do not want to slow down local commitsYou want instant feedback before pushing
nano-stagedYou want a smaller, faster alternative to lint-stagedYou need advanced lint-staged features like custom resolvers
VS Code format-on-saveSolo developer, no CITeam projects where consistency must be enforced

FAQs

What does the "prepare" script do in package.json?
  • It runs automatically after npm install.
  • It ensures Husky's Git hooks are installed for every developer who clones the repo.
  • Without it, new developers would not have pre-commit hooks until they manually run npx husky init.
How does lint-staged know which files to process?
  • It collects files from git diff --staged (files you have git add-ed).
  • It matches staged files against the glob patterns in your config.
  • Only matching staged files are passed to the configured commands.
What happens if ESLint fails during the pre-commit hook?
  • The commit is blocked and does not go through.
  • Commands run in sequence per glob -- if ESLint fails, Prettier does not run.
  • Fix the ESLint errors and try committing again.
How do I bypass the pre-commit hook in an emergency?
git commit --no-verify -m "WIP: work in progress"

Use this sparingly for debugging or WIP commits. CI will still catch issues.

Gotcha: Why are hooks not running after a fresh clone?
  • Git hooks live in .git/hooks/, which is not committed to the repo.
  • New developers must run npm install, which triggers the prepare script.
  • Ensure "prepare": "husky" is in your package.json scripts.
Can I run type-checking in the pre-commit hook?
# .husky/pre-commit
npx lint-staged
npx tsc --noEmit
  • Yes, but tsc --noEmit checks the entire project, not just staged files.
  • This can take 10+ seconds on large projects.
  • Consider running tsc only in CI to keep hooks fast.
How do I use Biome with lint-staged instead of ESLint + Prettier?
{
  "lint-staged": {
    "*.{ts,tsx,js,jsx,json,css}": [
      "biome check --write --no-errors-on-unmatched"
    ]
  }
}
Gotcha: What happens with partial staging (git add -p)?
  • lint-staged operates on the full file, not just the staged hunks.
  • This means unstaged changes in the same file are also processed.
  • For critical cases, commit the full file instead of partial staging.
How do I add commit message linting with commitlint?
npm install --save-dev @commitlint/cli @commitlint/config-conventional
echo "npx commitlint --edit \$1" > .husky/commit-msg

This enforces conventional commit messages like feat:, fix:, docs:.

Does lint-staged re-stage files after fixing them?
  • Yes. Fixed files are automatically re-staged so the commit includes the fixes.
  • You do not need to manually git add files after ESLint or Prettier auto-fix.
Why does ESLint warn about ignored files when run through lint-staged?
  • lint-staged passes explicit file paths to ESLint.
  • Files matching your ESLint ignore patterns trigger warnings when passed explicitly.
  • Add --no-warn-ignored to the ESLint command in your lint-staged config.
What TypeScript issues can ESLint auto-fix in the pre-commit hook?
  • Remove unused imports.
  • Add the type keyword to type-only imports.
  • Fix consistent-type-imports violations.
  • Reorder imports according to import/order rules.