Husky: The Complete Guide to Git Hooks in JavaScript Projects

Set up Git hooks that lint, format and validate commits — with recipes for lint-staged and commitlint

Samod Alex
•
7 min read
Husky: The Complete Guide to Git Hooks in JavaScript Projects

Researched on 4 October 2026 against husky 9.1.7, the latest release at the time of writing. Commands and behavior are taken from the official docs and release notes listed in the Sources section at the end.

Every team has shipped a commit that broke the build, failed the linter, or carried a message nobody could decode six months later. Code review and CI catch most of these, but they catch them late — after the push, after the context switch, after someone else has already pulled the mess. Husky moves those checks to the moment you type git commit or git push, so the feedback arrives in seconds on your own machine.

This guide covers what Husky is, how it works, how to set it up today, and how to avoid the pitfalls that trip up most teams.


TL;DR

  • Husky is a small npm package that makes Git hooks easy to share across a team. MIT licensed, maintained by Typicode.

  • Setup in version 9 is two commands: npm install --save-dev husky and npx husky init.

  • A hook is just a file in the .husky/ folder. The file .husky/pre-commit runs before every commit, and its contents are ordinary shell commands.

  • Pair it with lint-staged (run linters on staged files only) and commitlint (enforce commit message conventions).

  • Hooks can always be bypassed locally, so run the same checks in CI. Treat hooks as fast feedback, not enforcement.

  • If your hook files still start with the two-line husky.sh header from v8, remove it. It will fail in v10.


1. Git hooks in 60 seconds

Git can run a script automatically at defined points in its workflow. These scripts are called hooks. The ones you will use most often:

Hook

When it runs

Typical use

pre-commit

Before a commit is created. Non-zero exit aborts.

Lint, format, run fast tests

commit-msg

After you write a message, before the commit completes.

Enforce message conventions

pre-push

Before a push. Non-zero exit aborts.

Type-check, full tests

The pre-commit and commit-msg hooks can be skipped with --no-verify.

By default Git looks for hooks in .git/hooks. That directory is never committed, so hooks living there cannot be shared — every developer would have to copy scripts by hand. That is the problem Husky solves.

2. What Husky is

Husky is an npm package that stores your hooks inside the repository and wires them into Git automatically when a teammate installs dependencies.

  • License: MIT

  • Latest version: 9.1.7 (November 2024)

  • Size: ~2 kB gzipped, zero dependencies

  • Adoption: ~35.8 million weekly downloads, used in over 1.5 million GitHub projects including Next.js, webpack, Angular, VS Code, Zod and Rollup

3. A short history, and why old tutorials contradict each other

Husky has changed its configuration style several times. If a tutorial doesn't match what you see, check which major version it targets.

Era

How hooks were defined

0.x–4.x

JavaScript config in package.json or .huskyrc

5.x–6.x

Switched to files in .husky/

7.x–8.x

husky install + husky add; hooks had a two-line header sourcing husky.sh

9.x

husky init; hooks are plain shell files

Why the shift from JS config to files? Before v5, Husky installed every possible Git hook into .git/hooks, each launching a Node script to check your config. That started Node on every Git operation even when nothing was defined. The fix came from Git 2.9's core.hooksPath, which lets Git read hooks from a committed folder. No JavaScript middleman, one source of truth.

What changed in v9 (January 2024): husky init replaced a three-step setup with one command. Adding a hook became "create a file." husky install was removed. Since v9.1, locally installed tools can be called directly in hooks without npx. The old shebang and husky.sh lines were deprecated — hooks containing them will fail in v10.

4. Quick start with Husky 9

npm install --save-dev husky
npx husky init

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

git commit -m "Keep calm and commit"
# your test script runs before the commit is created

Commit the .husky/ folder and package.json so teammates get the same hooks.

5. How Husky 9 works under the hood

  1. prepare runs after install. npm runs prepare after npm install, triggering husky, which sets things up.

  2. Husky sets core.hooksPath to .husky/_, so Git reads hooks from there instead of .git/hooks.

  3. Hooks run with sh. Write POSIX-compatible shell unless your whole team can run Bash.

If you uninstall Husky, restore normal behavior with git config --unset core.hooksPath.

6. Writing hooks

A hook is a file named exactly after the Git hook:

echo "npm test" > .husky/pre-commit

Multiple commands go on separate lines:

# .husky/pre-commit
npm run lint
npm test

On Husky 9.1+, locally installed tools can be called directly (jest, eslint) without npx.

Some hooks receive arguments from Git. For commit-msg, $1 is the path to the file holding the message. The old HUSKY_GIT_PARAMS variable no longer exists.

Debugging: use HUSKY=2 git commit -m "debug run" for verbose output.


7. Recipes: lint-staged, commitlint, pre-push

lint-staged: run tasks only on staged files

Running a linter across a whole project on every commit is slow. lint-staged passes only staged files to your tools.

npm install --save-dev lint-staged
# .husky/pre-commit
npx lint-staged

Configure in package.json:

{
  "lint-staged": {
    "*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"],
    "*.{json,md,css}": "prettier --write"
  }
}

Watch for concurrent tasks. Overlapping globs that both edit files can race. Use negation patterns:

{
  "lint-staged": {
    "!(*.ts)": "prettier --write",
    "*.ts": ["eslint --fix", "prettier --write"]
  }
}

Type-checking caveat. lint-staged appends filenames to commands, which makes tsc ignore your tsconfig.json. Use a function config:

// lint-staged.config.mjs
export default {
  '*.{ts,tsx}': [() => 'tsc --noEmit', 'prettier --write'],
}

commitlint: enforce commit message conventions

If your team uses Conventional Commits (feat: ..., fix: ...):

npm install --save-dev @commitlint/cli @commitlint/config-conventional
echo "module.exports = { extends: ['@commitlint/config-conventional'] }" > commitlint.config.js
echo "npx --no -- commitlint --edit \$1" > .husky/commit-msg

A typical complete setup

.husky/
  pre-commit    ->  npx lint-staged
  commit-msg    ->  npx --no -- commitlint --edit $1
  pre-push      ->  npm run typecheck && npm test

8. Skipping and disabling hooks

Single command: git commit -m "WIP" -n (or --no-verify)

For commands without --no-verify: HUSKY=0 git rebase main

Globally on your machine: add export HUSKY=0 to ~/.config/husky/init.sh


9. CI, Docker and production installs

Disable in CI. In GitHub Actions: env: HUSKY: 0

Handle missing dev dependency. In production installs where Husky isn't installed, prevent prepare from failing:

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

Run the same checks in CI. Hooks are a local convenience. Anyone can skip them, so your pipeline must run lint, tests and commitlint independently.


10. Monorepos and projects not at the Git root

Given a layout where package.json is in a subfolder:

{
  "scripts": {
    "prepare": "cd .. && husky frontend/.husky"
  }
}
# frontend/.husky/pre-commit
cd frontend
npm test

11. Package manager notes

Manager

Install

Init

npm

npm install --save-dev husky

npx husky init

pnpm

pnpm add --save-dev husky

pnpm exec husky init

Yarn

yarn add --dev husky

Manual: use postinstall instead of prepare

Bun

bun add --dev husky

bunx husky init

Yarn doesn't support prepare the same way. Use postinstall: "husky" instead. If your package is published, add pinst to disable hooks in prepack/postpack.


12. Node version managers and Git GUIs

If Git runs from a GUI and Node comes from nvm/fnm/Volta/etc., hooks can fail with command not found because the GUI never sources your shell profile.

Fix by adding version manager initialization to ~/.config/husky/init.sh:

export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"

13. Troubleshooting

Symptom

Fix

Hooks don't run

Filename must be exactly pre-commit (not precommit or pre-commit.sh). Check git config core.hooksPath. Confirm Git ≥ 2.9.

Hooks not installed after clone

prepare didn't run. Check --ignore-scripts or missing prepare in package.json.

command not found in GUI

PATH/version manager issue. See section 12.

.git/hooks stopped working after uninstall

Run git config --unset core.hooksPath.

prepare fails in production

Use `husky

Deprecation warning about husky.sh

Delete the two header lines from hook files.


14. Migrating from v8 to v9

Version 9 is backward compatible with v8. Three edits:

  1. In package.json, change "prepare": "husky install" to "prepare": "husky".

  2. In each hook file, delete the shebang line and the husky.sh sourcing line. Leave only your commands.

  3. Move any ~/.huskyrc code to ~/.config/husky/init.sh, and replace HUSKY_DEBUG=1 with HUSKY=2.

Do this before v10 drops — hooks with the old header lines will fail.


15. Husky vs the alternatives

Tool

Approach

Best for

Husky

Shell files in .husky/, wired via core.hooksPath

JS/TS projects wanting a tiny, native-feeling tool

lefthook

Go binary, YAML config, built-in parallelism

Polyglot repos needing file filtering and parallel tasks

simple-git-hooks

Zero-dependency, configured in package.json

Small projects preferring config over files

Raw core.hooksPath

Point Git at a committed folder yourself

Teams that want no tooling at all


16. Criticisms and trade-offs

  • The v5 transition was rough. Moving from JS config to files was breaking, and the brief non-MIT licensing in v5 pushed some teams to alternatives. v6 returned to MIT.

  • Hooks are advisory. Anyone can skip with --no-verify or HUSKY=0. CI must repeat your checks.

  • It rewrites a Git setting. core.hooksPath means Husky doesn't coexist with other tools that expect .git/hooks. Pick one hook manager per repo.


17. Best practices checklist

  • Keep pre-commit fast (a few seconds). Use lint-staged.

  • Put slow checks (type-check, full tests) in pre-push or CI.

  • Commit the .husky/ folder and the prepare script.

  • Repeat every hook check in CI. Hooks are not enforcement.

  • Write hooks in POSIX shell.

  • Use husky || true so production installs don't break.

  • Set HUSKY: 0 in CI jobs.

  • Remove deprecated husky.sh header lines before v10.

  • Document how to bypass hooks responsibly so people don't delete them.


18. FAQ

Do I need lint-staged? No. Husky can run any command. lint-staged is useful when you want tools to run only on staged files, keeping hooks fast on large codebases.

Does it work with pnpm, Yarn and Bun? Yes. See section 11 for per-manager setup.

Does it work in Git GUI clients? Yes, with one caveat: add your version manager's init to ~/.config/husky/init.sh (section 12).

How do I uninstall?npm uninstall husky, remove prepare and .husky/, then git config --unset core.hooksPath.


Sources

Research done on 4 October 2026. Statistics will have changed since.

Husky

Companion tools

Git

Alternatives

Comments (0)

Join the discussion by logging into your account.

No comments yet. Be the first to comment!

Samod Alex

Passionate developer sharing knowledge about modern web technologies and best practices.

Subscribe to Samod Alex's Newsletter

Direct email dispatches when new stories are published. Zero algorithms.