homesponsors ⭐
 
   
🔍

Git hooks with Husky

July 12, 2026

Git hooks run scripts around local Git events (pre-commit, commit-msg, pre-push, and others). They are a good place for fast checks - lint, format, and lightweight validation - before bad changes leave your machine. Hooks are not a substitute for CI; treat them as an early filter.

Husky wires those hooks through Git's core.hooksPath. Hook files live in .husky/ as plain shell scripts, and a prepare script installs them after npm install so every clone gets the same setup.

This post covers Husky v9 setup, common hooks, lint-staged, skipping hooks in CI, pitfalls, and a runnable demo that blocks messy commits.

Prerequisites

  • Node.js version 26
  • A Git repository (git init if you are starting fresh)
  • npm i -D husky
  • Optional but recommended: npm i -D lint-staged plus ESLint and Prettier

Setup

Install Husky and initialize it:

npm i -D husky
npx husky init

husky init creates .husky/pre-commit and adds a prepare script to package.json:

{
"scripts": {
"prepare": "husky"
}
}

prepare runs after local npm install, so teammates get hooks without a separate setup step. Replace the default pre-commit body with the checks you want.

Mental model

PieceRole
prepare / huskyPoints Git at .husky/ via core.hooksPath
.husky/<hook>Shell script Git runs for that event
lint-stagedRuns tools only on staged files (typical pre-commit target)
CIStill runs full lint/test; hooks are local only

Husky does not invent a new hook system - it configures native Git hooks and keeps the scripts in the repo.

Pre-commit with lint-staged

Running eslint . or prettier --write on the whole tree from pre-commit gets slow. lint-staged limits work to staged files.

npm i -D lint-staged eslint prettier
// package.json
{
"lint-staged": {
"*.{js,ts}": ["eslint --fix", "prettier --write"]
}
}
# .husky/pre-commit
npx lint-staged

On git commit, Git runs .husky/pre-commitlint-staged → ESLint/Prettier on matching staged paths. Auto-fixed files are restaged. If a check exits non-zero, the commit is aborted.

Keep pre-commit fast and file-local. Put slow or whole-repo work in pre-push or CI.

Other useful hooks

commit-msg - validate the message file Git passes as $1:

# .husky/commit-msg
message=$(cat "$1")
if echo "$message" | grep -qE '^(WIP|wip)($|[^a-zA-Z])'; then
echo "WIP commits are blocked by the commit-msg hook"
exit 1
fi

pre-push - run a quicker subset of CI before a push:

# .husky/pre-push
npm test

Create a hook by adding an executable script under .husky/ with the Git hook name. Prefer POSIX shell for Windows compatibility.

Skipping hooks

Skip once with Git's no-verify flag:

git commit -m "emergency fix" -n

Disable Husky for a command or environment:

HUSKY=0 git commit -m "skip hooks"

Set HUSKY=0 (or HUSKY: 0 in CI env) on CI/Docker so installs do not try to configure local hooks. If production installs omit devDependencies, use a safe prepare script so missing husky does not fail the install:

{
"scripts": {
"prepare": "husky || true"
}
}

Pitfalls

  • Hooks are local - they do not run on the remote; CI still matters.
  • --no-verify / HUSKY=0 - anyone can bypass hooks; do not rely on them for security.
  • GUI + Node version managers - GUI clients often lack the shell PATH where nvm/fnm put Node. Source your version manager from ~/.config/husky/init.sh.
  • Nested package vs Git root - Husky will not install into a parent ../ by default. In a monorepo, install Husky at the repo root (or adjust prepare as in the Husky how-to).
  • Slow pre-commit - full test suites belong in pre-push or CI; keep commit hooks on staged files.

When to use what

ApproachGood for
Husky + lint-stagedFast lint/format on staged files before commit
Husky commit-msgLightweight message conventions locally
Husky pre-pushShort tests before sharing a branch
CI onlyFull lint, build, and test on every change

Pair Husky with ESLint and Prettier for the usual Node.js quality gate on commit.