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.

2023

Formatting Node.js codebase with Prettier

July 3, 2023

Formatting helps to stay consistent with code style throughout the whole codebase. Include format script in pre-hooks (pre-commit or pre-push). This post covers Prettier setup with JavaScript and TypeScript code.

Start by installing the prettier package as a dev dependency.

npm i prettier -D

Specify rules inside the .prettierrc config file.

{
"singleQuote": true,
"trailingComma": "all"
}

Add format script in the package.json file.

{
"scripts": {
// ...
"format": "prettier --write \"{src,test}/**/*.{js,ts}\""
}
}

Notes

If you use Eslint, install the eslint-config-prettier package as a dev dependency and update the Eslint configuration to use the Prettier config.

// eslint.config.js
// ...
import eslintConfigPrettier from 'eslint-config-prettier';
export default [
// ...
eslintConfigPrettier
];

Using Visual Studio Code, you can install a prettier-vscode extension and activate formatting when file changes are saved.