↓Skip to main content

Git Pre-commit Hooks with Husky 9, ESLint, and lint-staged

··3 mins
Sebastian Scheibe
Author
Sebastian Scheibe
Table of Contents

Use Husky to run a Git hook, lint-staged to select the files being committed, and ESLint to check those files. A failing lint command stops the commit so you can fix the issue first.

This example uses Husky 9.1.7, ESLint 10.9.1, and lint-staged 17.4.1. These are explicit example versions, not a claim about the latest releases. Use Node.js 24 or newer and an npm-based Git repository.

Install the tools #

In an existing project, run:

npm install --save-dev --save-exact [email protected] [email protected] [email protected]
npx husky init

For a new project, run git init and npm init -y first.

npx husky init creates .husky/pre-commit and adds a prepare script to package.json. Husky 9 uses "prepare": "husky". Replace the generated hook’s npm test command with:

npx lint-staged

The file .husky/pre-commit should contain that command. It does not need the older _/husky.sh bootstrap lines. Commit this hook file; Husky’s generated .husky/_ directory is ignored.

Configure ESLint #

If your project already has an ESLint configuration, keep it and adapt the staged-file pattern below. For a minimal JavaScript example, create eslint.config.mjs:

export default [
  {
    files: ["**/*.{js,mjs}"],
    languageOptions: {
      ecmaVersion: "latest",
      sourceType: "module",
    },
    rules: {
      "no-unused-vars": "error",
      "no-undef": "error",
    },
  },
];

This is an ESLint flat configuration. It deliberately enables just two rules so the example is easy to reproduce. A real application will need rules and environment globals appropriate to its codebase. TypeScript and JSX require additional parser or plugin configuration before adding those file types to the pattern.

Configure lint-staged #

Add these entries to package.json, merging them with your existing settings:

{
  "scripts": {
    "prepare": "husky",
    "lint": "eslint ."
  },
  "lint-staged": {
    "*.{js,mjs}": "eslint --fix"
  },
  "devDependencies": {
    "eslint": "10.9.1",
    "husky": "9.1.7",
    "lint-staged": "17.4.1"
  }
}

lint-staged passes the matching staged filenames to ESLint. Automatically fixable changes are staged by lint-staged; other errors prevent the commit. The pattern matches JavaScript files in subdirectories too.

Commit the lockfile so collaborators install the same dependency versions. A normal npm install or npm ci runs prepare to configure Husky in the checkout.

Check that the hook rejects an error #

Create example.js with an unused variable:

const unused = 1;
export const answer = 42;

Stage it and attempt a commit:

git add example.js
git commit -m "Try the lint hook"

ESLint should report no-unused-vars, and the commit should fail. Remove the unused declaration, stage the file again, and retry. The commit should succeed.

Run npm run lint to check the whole project. Keep linting and tests in CI as well: a local Git hook can be bypassed, and linting only covers the rules you configure. The Node.js Buffer regression is a useful example of why behavior also needs tests.

References #

The original example repository accompanies the earlier version of this article. The configuration above is the updated example.