GitHusky (JS Git Hooks)

Husky: Git Hooks for JavaScript Projects

Husky is a Node.js tool that makes Git hooks easy to share across a team. Without Husky, Git hooks live in .git/hooks/ — a directory that is not committed to version control. Every developer would have to manually copy hook scripts after cloning. Husky solves this by storing hooks in a .husky/ directory that IS committed, and using an npm prepare script to install them automatically after every npm install.

Installation and setup (Husky v9+)

Install Husky

Bash
# Install as a dev dependency
npm install --save-dev husky

# Initialise Husky (creates .husky/ directory + adds prepare script)
npx husky init

# Output:
# .husky/pre-commit created
# package.json 'prepare' script added

package.json after husky init

JSON
{
  "scripts": {
    "prepare": "husky"
  },
  "devDependencies": {
    "husky": "^9.0.0"
  }
}
The prepare script
The `prepare` script runs automatically on `npm install`. When a teammate clones the repo and runs `npm install`, Husky is installed into `.git/hooks/` without any manual steps.
Creating hooks

After npx husky init, a .husky/pre-commit file is created. Edit it to add your hook commands.

.husky/pre-commit — simple linting hook

Bash
#!/usr/bin/env sh
npm run lint

Add more hooks manually

Bash
# Create a commit-msg hook
echo "npx commitlint --edit $1" > .husky/commit-msg

# Create a pre-push hook
echo "npm test" > .husky/pre-push

# Make them executable
chmod +x .husky/commit-msg .husky/pre-push

The hook file runs as a shell script. $1 in commit-msg is the path to the temp file containing the commit message.

lint-staged: only lint what changed

Running ESLint or Prettier on the entire codebase on every commit is slow. lint-staged runs linters only on the files currently staged for commit — making hooks much faster.

Install lint-staged

Bash
npm install --save-dev lint-staged

package.json — lint-staged configuration

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

.husky/pre-commit — run lint-staged

Bash
#!/usr/bin/env sh
npx lint-staged

Now when you run git commit, lint-staged:

  1. Identifies which files are staged (git diff --name-only --cached).

  2. Runs ESLint --fix only on staged JS/TS files.

  3. Runs Prettier --write only on staged files.

  4. Re-stages the auto-fixed files.

  5. If any check still fails after auto-fix, the commit is blocked.

commit-msg hook with commitlint

commitlint enforces conventional commit message format. Combined with Husky, every commit message is validated before it is saved.

Install commitlint

Bash
npm install --save-dev @commitlint/cli @commitlint/config-conventional

commitlint.config.js

JS
module.exports = {
  extends: ['@commitlint/config-conventional'],
}

.husky/commit-msg

Bash
#!/usr/bin/env sh
npx --no -- commitlint --edit "$1"

Examples of valid and invalid commit messages

Bash
# VALID: conventional commit format
git commit -m "feat: add user authentication"
git commit -m "fix: resolve login redirect loop"
git commit -m "docs: update API documentation"
git commit -m "chore: upgrade dependencies"

# INVALID: blocked by commitlint
git commit -m "added some stuff"
# ✖   subject may not be empty [subject-empty]
# ✖   type may not be empty [type-empty]

git commit -m "Added login feature"
# ✖   type may not be empty [type-empty]
Complete Husky + lint-staged + commitlint setup

package.json — full configuration

JSON
{
  "scripts": {
    "prepare": "husky",
    "lint": "eslint .",
    "lint:fix": "eslint . --fix",
    "format": "prettier --write .",
    "test": "vitest run"
  },
  "lint-staged": {
    "*.{js,jsx,ts,tsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{css,json,md,yaml}": [
      "prettier --write"
    ]
  },
  "devDependencies": {
    "husky": "^9.0.0",
    "lint-staged": "^15.0.0",
    "@commitlint/cli": "^19.0.0",
    "@commitlint/config-conventional": "^19.0.0",
    "eslint": "^8.0.0",
    "prettier": "^3.0.0"
  }
}

.husky/pre-commit

Bash
#!/usr/bin/env sh
npx lint-staged

.husky/commit-msg

Bash
#!/usr/bin/env sh
npx --no -- commitlint --edit "$1"

.husky/pre-push

Bash
#!/usr/bin/env sh
npm test
Husky v8 vs v9 changes

Feature

Husky v8

Husky v9

Init command

npx husky-init && npm install

npx husky init

Hook directory

.husky/

.husky/ (same)

prepare script

husky install

husky

Hook format

#!/usr/bin/env sh + . .husky/_/husky.sh

Simple shell script, no source needed

Bypass variable

HUSKY=0 git commit

HUSKY=0 git commit (same)

CI behavior

Needs special handling

Auto-skips in CI if HUSKY=0

Bypassing hooks

Skip hooks when necessary

Bash
# Skip all hooks for one commit
git commit --no-verify -m "chore: emergency fix"

# Skip Husky specifically (useful in CI scripts)
HUSKY=0 git commit -m "chore: automated commit"
Warning
`git commit --no-verify` bypasses all hooks including `pre-commit` and `commit-msg`. Reserve it for genuine emergencies. If your hooks are failing for a legitimate reason (e.g. too slow, false positives), fix the root cause rather than routinely bypassing.
CI environments

In CI, you typically do not want Husky installing hooks because:

  • The npm install in CI should not take extra time installing Git hooks.

  • CI runs on clean checkouts where hooks are irrelevant (CI has its own quality gates).

  • Some CI environments fail if git config calls fail due to permissions.

Skip Husky in CI

Bash
# Option A: set HUSKY=0 environment variable in your CI workflow
# GitHub Actions:
env:
  HUSKY: 0

# Option B: modify the prepare script to skip in CI
# package.json:
# "prepare": "node -e "if(process.env.CI) process.exit(0)" && husky"
Onboarding new team members

What happens when a new dev clones the repo

Bash
git clone https://github.com/org/repo.git
cd repo

npm install
# postinstall runs: husky
# Husky automatically installs the hooks into .git/hooks/

# Developer is now protected by all hooks — zero manual setup
Tip
Add a `check:hooks` script to `package.json` that prints the current hook status — handy for diagnosing issues when hooks seem to not be running.