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
With lint-staged (recommended)
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:
- Finds staged
.ts/.tsx/.js/.jsxfiles - Runs ESLint with auto-fix
- Runs Prettier
- Re-stages the fixed files
- 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
| Hook | Purpose | Speed |
|---|---|---|
pre-commit | Lint + format staged files | Fast (< 3s with lint-staged) |
commit-msg | Validate commit message | Instant |
pre-push | Type check + tests | Can 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=0in 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 vianpm install