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

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 huskyandnpx husky init.A hook is just a file in the
.husky/folder. The file.husky/pre-commitruns 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.shheader 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
prepareruns after install. npm runsprepareafternpm install, triggeringhusky, which sets things up.Husky sets
core.hooksPathto.husky/_, so Git reads hooks from there instead of.git/hooks.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:
In
package.json, change"prepare": "husky install"to"prepare": "husky".In each hook file, delete the shebang line and the
husky.shsourcing line. Leave only your commands.Move any
~/.huskyrccode to~/.config/husky/init.sh, and replaceHUSKY_DEBUG=1withHUSKY=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-verifyorHUSKY=0. CI must repeat your checks.It rewrites a Git setting.
core.hooksPathmeans Husky doesn't coexist with other tools that expect.git/hooks. Pick one hook manager per repo.
17. Best practices checklist
Keep
pre-commitfast (a few seconds). Use lint-staged.Put slow checks (type-check, full tests) in
pre-pushor CI.Commit the
.husky/folder and thepreparescript.Repeat every hook check in CI. Hooks are not enforcement.
Write hooks in POSIX shell.
Use
husky || trueso production installs don't break.Set
HUSKY: 0in CI jobs.Remove deprecated
husky.shheader 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
Official docs: Introduction, Get started, How To, Troubleshoot, Migrate from v4
Author's blog post: Why husky has dropped conventional JS config
Companion tools
lint-staged: github.com/lint-staged/lint-staged
commitlint: Guide: Local setup
Git
Alternatives
lefthook: Evil Martians introduction
simple-git-hooks: github.com/toplenboren/simple-git-hooks
Published via ZyVOP — Write once in Markdown, auto-backup to GitHub, and syndicate to Dev.to, Medium & Hashnode in 1 click.





