Git hooks with Husky
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 initif you are starting fresh) npm i -D husky- Optional but recommended:
npm i -D lint-stagedplus ESLint and Prettier
Setup
Install Husky and initialize it:
npm i -D husky
npx husky inithusky 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
prepare / husky — Points Git at .husky/ via core.hooksPath — Shell script Git runs for that event
.husky/<hook>lint-staged — Runs tools only on staged files (typical pre-commit target)
CI — Still 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-stagedOn git commit, Git runs .husky/pre-commit → lint-staged → ESLint/Prettier on matching staged paths. Auto-fixed files are restaged. If a check exits non-zero, the commit is aborted.
Get Željko Šević’s stories in your inbox
Join Medium for free to get updates from this writer.
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
fipre-push - run a quicker subset of CI before a push:
# .husky/pre-push
npm testCreate 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" -nDisable 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
PATHwherenvm/fnmput 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 adjustprepareas in the Husky how-to). - Slow pre-commit — full test suites belong in
pre-pushor CI; keep commit hooks on staged files.
When to use what
Husky + lint-staged — Fast lint/format on staged files before commit
Husky commit-msg — Lightweight message conventions locally
Husky pre-push — Short tests before sharing a branch
CI only — Full lint, build, and test on every change
Pair Husky with ESLint and Prettier for the usual Node.js quality gate on commit.
Need help with your project?
Get personalized advice on your architecture, code, or career in a 45-minute 1-on-1 consultation.







