Setting up Husky in a new project Jump to heading
Git hooks live in .git/hooks, which is not versioned, so a hook written by one developer never reaches anyone else. Husky solves that by keeping hooks in a versioned .husky/ directory and pointing Git at it with core.hooksPath whenever dependencies are installed. Version 9 reduced it to almost nothing: one dependency, one prepare script, and plain shell files. Setting it up in a new project takes minutes, but a few choices made at the start β which hooks, what they run, how they behave in CI β decide whether the team keeps them or learns to bypass them. This page walks through a clean setup, within local hook configuration with Husky.
When to use this approach Jump to heading
- You are starting a new JavaScript or TypeScript repository, or one with a
package.jsonat its root. - You want every contributor to get the same Git hooks automatically on
npm install. - No hook manager is in place yet. For an existing repository with ad hoc hooks, see migrating from legacy Git hooks to Husky v9.
- For repositories without Node, consider the pre-commit framework for polyglot repos instead.
Step 1 β Install Husky and initialise it Jump to heading
husky init adds the dependencyβs prepare script, creates .husky/ and writes a sample pre-commit hook.
npm install --save-dev husky
npx husky init
cat package.json | grep -A2 '"scripts"' # "prepare": "husky"
ls .husky/ # pre-commit The prepare script runs automatically after npm install, so every contributor who installs dependencies gets hooks configured without doing anything else.
# Verification: Git now looks for hooks in .husky/_
git config --get core.hooksPath # .husky/_ Step 2 β Write the pre-commit hook Jump to heading
Hooks in Husky v9 are plain shell scripts with no boilerplate. Start with something fast that every commit benefits from: formatting and linting staged files through lint-staged.
npm install --save-dev lint-staged
printf 'npx lint-staged\n' > .husky/pre-commit
cat > .lintstagedrc.json <<'EOF'
{
"*.{ts,tsx,js}": ["eslint --cache --fix", "prettier --write --cache"],
"*.{json,md,yml}": "prettier --write --cache"
}
EOF Keep this hook fast β a few seconds at most. Slow checks belong in pre-push or CI; the reasoning is in speeding up slow lint-staged runs.
Step 3 β Add a commit-msg hook Jump to heading
A commit-msg hook receives the path of the message file as its first argument. Use it to enforce your message convention, for example Conventional Commits with commitlint.
npm install --save-dev @commitlint/cli @commitlint/config-conventional
printf "export default { extends: ['@commitlint/config-conventional'] };\n" > commitlint.config.mjs
printf 'npx --no -- commitlint --edit "$1"\n' > .husky/commit-msg # Verification: a bad message is rejected, a good one accepted
git commit --allow-empty -m "stuff" ; echo "exit: $?" # rejected
git commit --allow-empty -m "chore: verify commit-msg hook" # accepted Step 4 β Add pre-push for slower checks Jump to heading
Checks that take tens of seconds β a type check, a quick test subset β run once per push instead of on every commit.
printf 'npx tsc --noEmit\nnpm test -- --onlyChanged\n' > .husky/pre-push The trade-offs of what to run before pushing are covered in running a fast test subset in a pre-push hook.
Step 5 β Make it behave in CI and production installs Jump to heading
The prepare script runs on every npm install, including in CI and in production images where there is no .git directory or no need for hooks. Husky v9 exits quietly when .git is missing, but installs that omit dev dependencies fail if prepare references a missing package. Guard it.
{
"scripts": {
"prepare": "husky || true"
}
} The fuller treatment, including HUSKY=0, is in skipping Husky in CI and production installs.
Step 6 β Document and verify the setup Jump to heading
Add a short section to the contributing guide: what each hook does, how long it should take, and what to do when it fails. Then verify on a fresh clone, which is what a new contributor experiences.
cd "$(mktemp -d)" && git clone "$REPO_URL" app && cd app
npm install
git config --get core.hooksPath # .husky/_
echo "const x = 1" > scratch.ts && git add scratch.ts && git commit -m "test: hooks on fresh clone" Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
Do I need the #!/bin/sh line or husky.sh sourcing from older versions? Jump to heading
No. Husky v9 hooks are plain scripts; the shebang and the sourcing line from v4βv8 are unnecessary and Husky warns about the old sourcing line. Files only need to exist in .husky/.
Can contributors opt out? Jump to heading
Individually, with git commit --no-verify for one commit, or HUSKY=0 in their environment to disable all hooks. That is acceptable because CI enforces the same checks; hooks are a convenience, not a security boundary.
Does Husky work with Git worktrees? Jump to heading
Yes. core.hooksPath is relative, so it resolves inside each worktreeβs checkout of .husky/. Run npm install in each worktree, or share node_modules deliberately.
Related Jump to heading
- Local Hook Configuration with Husky β the parent topic.
- Testing Git Hooks Before Sharing Them β checking hooks work before they reach the team.
- How to Enforce Conventional Commits with commitlint β the commit-msg check in depth.
- Using Husky with pnpm and Yarn β package-manager specifics.