Overview
Husky points Git at a .husky/ folder of plain shell scripts that live in the repository, so every contributor gets the same hooks after npm install. Typical uses: run linters and formatters on staged files before a commit, validate commit messages, run tests before a push. The current release is Husky 9.1 (the latest at the time of writing, 9.1.7).
Instructions
Step 1: Install and initialize
npm install --save-dev husky lint-staged
npx husky init
npx husky init creates .husky/pre-commit (containing npm test) and adds "prepare": "husky" to package.json. The prepare script runs on every npm install, which is what activates the hooks for each teammate. Other package managers: pnpm add --save-dev husky then pnpm exec husky init; bun add --dev husky then bunx husky init.
Husky 9 hook files are plain commands: no shebang, no . "$(dirname -- "$0")/_/husky.sh" line, and no npx is needed in front of locally installed tools. If you migrate a v8 project, delete those two old lines and replace "prepare": "husky install" with "prepare": "husky".
Step 2: Pre-commit hook
# .husky/pre-commit
lint-staged
{
"lint-staged": {
"*.{ts,tsx}": ["eslint --fix", "prettier --write"],
"*.{css,md,json}": ["prettier --write"]
}
}
lint-staged passes only the staged files to each command and re-stages what the commands fixed.
Step 3: Commit-message and pre-push hooks
# .husky/commit-msg ($1 is the path of the file holding the message)
npx --no -- commitlint --edit $1
# .husky/pre-push
npm test
Any non-zero exit code aborts the Git operation.
Step 4: Skip or disable hooks
git commit -m "wip: spike new parser" -n # skip pre-commit and commit-msg for one commit
HUSKY=0 git push # disable Husky for one command
In CI or Docker, set HUSKY=0 as an environment variable so npm ci does not install hooks. To disable for all projects (for example in a Git GUI), put export HUSKY=0 in ~/.config/husky/init.sh.
Examples
Example 1: "Run ESLint and Prettier on staged files before every commit"
npm install --save-dev husky lint-staged eslint prettier
npx husky init
echo "lint-staged" > .husky/pre-commit
Add the lint-staged block from Step 2 to package.json. Then stage a file with a lint error and run git commit -m "feat: add invoice export": ESLint prints the error, the commit is aborted, and nothing is created. Fix the file, stage it again, and the commit goes through.
Example 2: "The package.json is in frontend/ but the Git root is one level up"
{
"scripts": {
"prepare": "cd .. && husky frontend/.husky"
}
}
# frontend/.husky/pre-commit
cd frontend && npm test
Husky is told to use frontend/.husky as the hooks folder. Because Git runs hooks from the repository root, each hook changes into frontend first.
Example 3: "My hook cannot find node (nvm)"
Git GUIs and some terminals do not load your shell profile. Put the version-manager setup in ~/.config/husky/init.sh, which Husky sources before every hook:
# ~/.config/husky/init.sh
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
Guidelines
- Hooks are installed by the
preparescript, sonpm install --ignore-scriptsleaves a clone without hooks. Never rely on hooks alone for enforcement: run the same checks in CI. - Keep
pre-commitfast (staged files only); put the full test suite inpre-pushor CI. - To try a hook without committing, temporarily add
exit 1as its last line. - For a hook written in another language, make the hook a one-liner such as
node .husky/pre-commit.js. - lint-staged 17 requires Node.js 22.22.1+; on older Node pin
lint-staged@16. - Husky does not modify
.git/hooks: it setscore.hooksPathto.husky/_. If hooks silently do nothing, checkgit config core.hooksPathand thatprepareran. - Hooks are not a security boundary: anyone can bypass them with
-n.