Husky Complete Guide | Git Hooks for Linting, Formatting, and Commit Standards

Husky Complete Guide | Git Hooks for Linting, Formatting, and Commit Standards

이 글의 핵심

Husky runs scripts automatically on git events — pre-commit linting catches errors before they enter your repo, commitlint enforces commit message standards, and pre-push hooks prevent broken code from reaching CI. Set it up once and enforce quality for the whole team.

What Husky Does

Husky makes git hooks easy:

git commit → pre-commit hook → lint + format staged files
git commit → commit-msg hook → validate commit message format
git push   → pre-push hook  → run tests

Without Husky, hooks are per-developer local files (.git/hooks/) not shared in the repo. With Husky, they’re in package.json and installed automatically.


1. Installation

# Install Husky
npm install --save-dev husky

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

This adds to package.json:

{
  "scripts": {
    "prepare": "husky"
  }
}

And creates .husky/pre-commit with a sample hook.


2. Pre-commit Hook — Lint and Format

lint-staged runs linters only on staged files — much faster than linting everything.

npm install --save-dev lint-staged
// package.json
{
  "lint-staged": {
    "*.{ts,tsx,js,jsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{css,scss,json,md}": [
      "prettier --write"
    ]
  }
}
# .husky/pre-commit
npx lint-staged

Now every git commit automatically:

  1. Finds staged .ts/.tsx/.js/.jsx files
  2. Runs ESLint with auto-fix
  3. Runs Prettier
  4. Re-stages the fixed files
  5. Commits

Alternative: direct commands

# .husky/pre-commit
npm run lint
npm run format:check

3. Commit Message Validation — commitlint

Enforce conventional commit format: feat(scope): description

npm install --save-dev @commitlint/cli @commitlint/config-conventional
// commitlint.config.js
export default {
  extends: ['@commitlint/config-conventional'],
  rules: {
    // Customize if needed
    'type-enum': [2, 'always', [
      'feat',    // new feature
      'fix',     // bug fix
      'docs',    // documentation
      'style',   // formatting
      'refactor',// code change without feat/fix
      'test',    // tests
      'chore',   // build/tooling
      'perf',    // performance
      'ci',      // CI changes
      'revert',  // revert commit
    ]],
    'subject-max-length': [2, 'always', 100],
  },
}
# .husky/commit-msg
npx --no -- commitlint --edit $1

Valid commit messages:

feat(auth): add OAuth2 login
fix(api): handle null response from user endpoint
docs: update README installation steps
chore: upgrade dependencies to latest

Invalid (will be rejected):

WIP
update stuff
Fixed the bug

4. Pre-push Hook — Run Tests

Pre-commit should be fast (lint only). Run tests before push:

# .husky/pre-push
npm test -- --passWithNoTests

Or run type checking + tests:

# .husky/pre-push
npm run typecheck && npm test

5. Full Setup Example

// package.json
{
  "scripts": {
    "prepare": "husky",
    "lint": "eslint src --ext .ts,.tsx",
    "lint:fix": "eslint src --ext .ts,.tsx --fix",
    "format": "prettier --write \"src/**/*.{ts,tsx,css,json}\"",
    "format:check": "prettier --check \"src/**/*.{ts,tsx,css,json}\"",
    "typecheck": "tsc --noEmit",
    "test": "vitest run"
  },
  "lint-staged": {
    "src/**/*.{ts,tsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "src/**/*.{css,json,md}": [
      "prettier --write"
    ]
  }
}
# .husky/pre-commit
npx lint-staged
# .husky/commit-msg
npx --no -- commitlint --edit $1
# .husky/pre-push
npm run typecheck

6. Hooks for Different Scenarios

Block commits with TODO/FIXME

# .husky/pre-commit
if git diff --cached | grep -E '^\+.*TODO|^\+.*FIXME' > /dev/null; then
  echo "Error: staged changes contain TODO or FIXME comments"
  echo "Resolve them or use git commit --no-verify to skip"
  exit 1
fi

npx lint-staged

Prevent direct commits to main

# .husky/pre-commit
branch=$(git rev-parse --abbrev-ref HEAD)
if [ "$branch" = "main" ] || [ "$branch" = "master" ]; then
  echo "Direct commits to $branch are not allowed."
  echo "Create a feature branch and open a pull request."
  exit 1
fi

npx lint-staged

Check for large files

# .husky/pre-commit
MAX_SIZE=500  # KB

for file in $(git diff --cached --name-only); do
  if [ -f "$file" ]; then
    size=$(du -k "$file" | cut -f1)
    if [ $size -gt $MAX_SIZE ]; then
      echo "Error: $file is ${size}KB (max ${MAX_SIZE}KB)"
      echo "Add large files to .gitignore"
      exit 1
    fi
  fi
done

npx lint-staged

7. CI Integration

Husky hooks should NOT run in CI (CI has its own lint/test steps):

# .github/workflows/ci.yml
- name: Install dependencies
  run: npm ci
  env:
    HUSKY: 0  # disable Husky in CI

Or automatically skip in CI (Husky 9+ does this by default):

# Husky 9+ checks CI environment automatically
# The prepare script skips in CI environments
# (when CI=true env var is set, which GitHub Actions sets automatically)

8. Bypassing Hooks

When genuinely needed (emergency commits, fixing hook issues):

# Skip all hooks for one commit
git commit --no-verify -m "emergency: hotfix"

# Skip pre-push
git push --no-verify

# Disable Husky for the session
HUSKY=0 git commit -m "wip"

# Disable lint-staged for one commit
LINT_STAGED_SKIP=true git commit -m "wip"

Common Issues

Hook not executable

chmod +x .husky/pre-commit

Hooks not running after npm install

# Make sure prepare script exists
npm run prepare

ESLint errors blocking commits in CI

# Make sure HUSKY=0 is set in CI env

lint-staged running on deleted files

{
  "lint-staged": {
    "*.{ts,tsx}": "eslint --fix"  // lint-staged skips deleted files automatically
  }
}

Key Takeaways

HookPurposeSpeed
pre-commitLint + format staged filesFast (< 3s with lint-staged)
commit-msgValidate commit messageInstant
pre-pushType check + testsCan be slow (use wisely)
  • lint-staged makes pre-commit fast by only linting changed files
  • commitlint enforces commit message conventions for readable git history
  • Set HUSKY=0 in CI — CI has its own steps, don’t duplicate work
  • Keep hooks fast — developers bypass slow hooks with --no-verify
  • Hooks are version-controlled in .husky/ and installed via npm install