| name | git-hooks |
| description | Git hooks setup with Husky, lint-staged, lefthook, and native hooks for automated code quality. Use when user asks to "setup pre-commit hooks", "add husky", "lint on commit", "format before commit", "run tests on push", "setup git hooks", "validate commit messages", "enforce conventional commits", "detect secrets in commits", "configure lefthook", "share git hooks", "core.hooksPath", "hook bypass", "debug git hooks", or automate code quality checks. |
Git Hooks
Automate code quality, enforce conventions, and prevent bad commits with Git hooks.
Hook Types and Use Cases
Hook Trigger Common Use
─────────────────────────────────────────────────────────────────
pre-commit Before commit is created Lint, format, secret scan
prepare-commit-msg After default msg, before edit Add ticket ID, template
commit-msg After user writes message Enforce conventional commits
pre-push Before push to remote Full test suite, build check
pre-rebase Before rebase starts Prevent rebase on protected branches
post-merge After merge completes Reinstall deps, run migrations
post-checkout After checkout/switch Rebuild, clear caches
Husky v9 (Node.js Projects)
npm install -D husky
npx husky init
echo "npx lint-staged" > .husky/pre-commit
echo "npm test" > .husky/pre-push
echo 'npx --no -- commitlint --edit "$1"' > .husky/commit-msg
lint-staged Configuration
{
"lint-staged": {
"*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"],
"*.{json,md,yml,yaml}": "prettier --write",
"*.css": ["stylelint --fix", "prettier --write"],
"*.py": ["ruff check --fix", "ruff format"],
"*.go": ["gofmt -w", "go vet ./..."]
}
}
Advanced lint-staged (Function Config)
export default {
'*.ts': () => 'tsc --noEmit',
'*.{js,ts}': (filenames) => [
`eslint --fix ${filenames.join(' ')}`,
`prettier --write ${filenames.join(' ')}`,
],
'tests/**/*.ts': 'jest --findRelatedTests',
}
Lefthook (Modern Alternative)
npm install -D lefthook
npx lefthook install
pre-commit:
parallel: true
commands:
lint:
glob: "*.{js,ts,tsx}"
run: npx eslint --fix {staged_files}
format:
glob: "*.{js,ts,tsx,json,md}"
run: npx prettier --write {staged_files}
types:
glob: "*.{ts,tsx}"
run: npx tsc --noEmit
commit-msg:
commands:
validate:
run: 'echo "{1}" | npx commitlint --config .commitlintrc.yml'
pre-push:
commands:
test:
run: npm test
Lefthook advantages: YAML config, built-in parallel execution, glob filtering,
{staged_files} variable, Go binary (no Node dependency), native monorepo root: support.
Pre-commit Framework (Python/Polyglot)
pip install pre-commit
pre-commit install
pre-commit install --hook-type pre-push
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
args: ['--maxkb=500']
- id: detect-private-key
- id: check-merge-conflict
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.0
hooks:
- id: ruff
args: [--fix]
- repo: local
hooks:
- id: pytest
name: pytest
entry: pytest
language: system
pass_filenames: false
always_run: true
stages: [pre-push]
pre-commit run --all-files
pre-commit autoupdate
Custom Hook Scripts
Conventional Commits Enforcement
#!/bin/sh
commit_msg=$(cat "$1")
pattern="^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?(!)?: .{1,72}"
if ! echo "$commit_msg" | grep -qE "$pattern"; then
echo "ERROR: Invalid commit message format."
echo "Expected: type(scope): description"
echo "Example: feat(auth): add OAuth2 login flow"
exit 1
fi
Branch Naming Enforcement
#!/bin/sh
branch=$(git symbolic-ref --short HEAD)
pattern="^(feature|bugfix|hotfix|release|chore)/[a-z0-9._-]+$"
if ! echo "$branch" | grep -qE "$pattern"; then
echo "ERROR: Branch '$branch' doesn't match pattern."
echo "Use: feature/desc, bugfix/desc, hotfix/desc"
exit 1
fi
Secret Detection
#!/bin/sh
patterns="(PRIVATE.KEY|password\s*=|secret\s*=|api[_-]?key\s*=|AWS_SECRET|BEGIN RSA)"
staged=$(git diff --cached --name-only --diff-filter=ACM)
for file in $staged; do
if git show ":$file" | grep -qEi "$patterns"; then
echo "ERROR: Possible secret detected in $file"
exit 1
fi
done
File Size Limit
#!/bin/sh
max_size=1048576
for file in $(git diff --cached --name-only --diff-filter=ACM); do
size=$(git cat-file -s ":$file" 2>/dev/null || echo 0)
[ "$size" -gt "$max_size" ] && echo "ERROR: $file exceeds 1MB limit." && exit 1
done
Merge Conflict Markers
#!/bin/sh
for file in $(git diff --cached --name-only --diff-filter=ACM); do
if git show ":$file" | grep -qE "^(<<<<<<<|=======|>>>>>>>)"; then
echo "ERROR: Merge conflict markers in $file"; exit 1
fi
done
Hooks in Node.js / Python
#!/usr/bin/env node
const fs = require('fs');
const msg = fs.readFileSync(process.argv[2], 'utf8').trim();
if (!/^(feat|fix|docs|chore|refactor|test|perf|ci)(\(.+\))?: .{1,72}/.test(msg)) {
console.error('Invalid commit message. Use: type(scope): description');
process.exit(1);
}
import subprocess, sys
staged = subprocess.run(["git", "diff", "--cached", "--name-only", "--diff-filter=ACM"],
capture_output=True, text=True).stdout.strip().split("\n")
for f in [x for x in staged if x.endswith(".py")]:
if subprocess.run(["python", "-m", "py_compile", f]).returncode != 0:
print(f"Syntax error in {f}"); sys.exit(1)
Sharing Hooks with Team
mkdir .githooks && chmod +x .githooks/*
git config core.hooksPath .githooks
Bypassing Hooks
git commit --no-verify -m "hotfix: emergency production fix"
git push --no-verify
Debugging Hooks
git config core.hooksPath
git rev-parse --git-path hooks
bash -x .husky/pre-commit
chmod +x .githooks/pre-commit
Monorepo Patterns
Lefthook Monorepo
pre-commit:
commands:
frontend-lint:
root: packages/frontend/
glob: "*.{ts,tsx}"
run: npx eslint --fix {staged_files}
backend-lint:
root: packages/backend/
glob: "*.py"
run: ruff check --fix {staged_files}
lint-staged Monorepo
export default {
'packages/frontend/**/*.{ts,tsx}': (files) =>
`cd packages/frontend && npx eslint --fix ${files.join(' ')}`,
'packages/backend/**/*.py': (files) =>
`cd packages/backend && ruff check --fix ${files.join(' ')}`,
}
Affected-Only Pre-push
#!/bin/sh
changed=$(git diff --name-only @{push}..)
echo "$changed" | grep -q "^packages/frontend/" && (cd packages/frontend && npm test)
echo "$changed" | grep -q "^packages/backend/" && (cd packages/backend && npm test)
CI/CD Hook Equivalents
name: Code Quality
on: [push, pull_request]
jobs:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- run: npm ci
- run: npx eslint . && npx prettier --check .
- run: npm test
- run: npx commitlint --from ${{ github.event.pull_request.base.sha }}