git-hooks-automation
Master Git hooks setup with Husky, lint-staged, pre-commit framework, and commitlint. Automate code quality gates, formatting, linting, and commit message enforcement before code reaches CI.
- risk
- safe
- source
- community
- date added
- 2026-03-07
Git Hooks Automation
Automate code quality enforcement at the Git level. Set up hooks that lint, format, test, and validate before commits and pushes ever reach your CI pipeline ā catching issues in seconds instead of minutes.
When to Use This Skill
- User asks to "set up git hooks" or "add pre-commit hooks"
- Configuring Husky, lint-staged, or the pre-commit framework
- Enforcing commit message conventions (Conventional Commits, commitlint)
- Automating linting, formatting, or type-checking before commits
- Setting up pre-push hooks for test runners
- Migrating from Husky v4 to v9+ or adopting hooks from scratch
- User mentions "pre-commit", "commit-msg", "pre-push", "lint-staged", or "githooks"
Git Hooks Fundamentals
Git hooks are scripts that run automatically at specific points in the Git workflow. They live in .git/hooks/ and are not version-controlled by default ā which is why tools like Husky exist.
Hook Types & When They Fire
| Hook | Fires When | Common Use |
|---|---|---|
pre-commit | Before commit is created | Lint, format, type-check staged files |
prepare-commit-msg | After default msg, before editor | Auto-populate commit templates |
commit-msg | After user writes commit message | Enforce commit message format |
post-commit | After commit is created | Notifications, logging |
pre-push | Before push to remote | Run tests, check branch policies |
pre-rebase | Before rebase starts | Prevent rebase on protected branches |
post-merge | After merge completes | Install deps, run migrations |
post-checkout | After checkout/switch | Install deps, rebuild assets |
Native Git Hooks (No Framework)
# Create a pre-commit hook manually cat > .git/hooks/pre-commit << 'EOF' #!/bin/sh set -e # Run linter on staged files only STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(js|ts|jsx|tsx)$' || true) if [ -n "$STAGED_FILES" ]; then echo "š Linting staged files..." echo "$STAGED_FILES" | xargs npx eslint --fix echo "$STAGED_FILES" | xargs git add # Re-stage after fixes fi EOF chmod +x .git/hooks/pre-commit
Problem: .git/hooks/ is local-only and not shared with the team. Use a framework instead.
Husky + lint-staged (Node.js Projects)
The modern standard for JavaScript/TypeScript projects. Husky manages Git hooks; lint-staged runs commands only on staged files for speed.
Quick Setup (Husky v9+)
# Install npm install --save-dev husky lint-staged # Initialize Husky (creates .husky/ directory) npx husky init # The init command creates a pre-commit hook ā edit it: echo "npx lint-staged" > .husky/pre-commit
Configure lint-staged in package.json
{ "lint-staged": { "*.{js,jsx,ts,tsx}": [ "eslint --fix --max-warnings=0", "prettier --write" ], "*.{css,scss}": [ "prettier --write", "stylelint --fix" ], "*.{json,md,yml,yaml}": [ "prettier --write" ] } }
Add Commit Message Linting
# Install commitlint npm install --save-dev @commitlint/cli @commitlint/config-conventional # Create commitlint config cat > commitlint.config.js << 'EOF' module.exports = { extends: ['@commitlint/config-conventional'], rules: { 'type-enum': [2, 'always', [ 'feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'build', 'ci', 'chore', 'revert' ]], 'subject-max-length': [2, 'always', 72], 'body-max-line-length': [2, 'always', 100] } }; EOF # Add commit-msg hook echo "npx --no -- commitlint --edit \$1" > .husky/commit-msg
Add Pre-Push Hook
# Run tests before pushing echo "npm test" > .husky/pre-push
Complete Husky Directory Structure
project/ āāā .husky/ ā āāā pre-commit # npx lint-staged ā āāā commit-msg # npx --no -- commitlint --edit $1 ā āāā pre-push # npm test āāā commitlint.config.js āāā package.json # lint-staged config here āāā ...
pre-commit Framework (Python / Polyglot)
Language-agnostic framework that works with any project. Hooks are defined in YAML and run in isolated environments.
Setup
# Install (Python required) pip install pre-commit # Create config cat > .pre-commit-config.yaml << 'EOF' repos: # Built-in checks - 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-json - id: check-added-large-files args: ['--maxkb=500'] - id: check-merge-conflict - id: detect-private-key # Python formatting - repo: https://github.com/psf/black rev: 24.4.2 hooks: - id: black # Python linting - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.4.4 hooks: - id: ruff args: ['--fix'] - id: ruff-format # Shell script linting - repo: https://github.com/shellcheck-py/shellcheck-py rev: v0.10.0.1 hooks: - id: shellcheck # Commit message format - repo: https://github.com/compilerla/conventional-pre-commit rev: v3.2.0 hooks: - id: conventional-pre-commit stages: [commit-msg] EOF # Install hooks into .git/hooks/ pre-commit install pre-commit install --hook-type commit-msg # Run against all files (first time) pre-commit run --all-files
Key Commands
pre-commit install # Install hooks pre-commit run --all-files # Run on everything (CI or first setup) pre-commit autoupdate # Update hook versions pre-commit run <hook-id> # Run a specific hook pre-commit clean # Clear cached environments
Custom Hook Scripts (Any Language)
For projects not using Node or Python, write hooks directly in shell.
Portable Pre-Commit Hook
#!/bin/sh # .githooks/pre-commit ā Team-shared hooks directory set -e echo "=== Pre-Commit Checks ===" # 1. Prevent commits to main/master BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || echo "detached") if [ "$BRANCH" = "main" ] || [ "$BRANCH" = "master" ]; then echo "ā Direct commits to $BRANCH are not allowed. Use a feature branch." exit 1 fi # 2. Check for debugging artifacts if git diff --cached --diff-filter=ACM | grep -nE '(console\.log|debugger|binding\.pry|import pdb)' > /dev/null 2>&1; then echo "ā ļø Debug statements found in staged files:" git diff --cached --diff-filter=ACM | grep -nE '(console\.log|debugger|binding\.pry|import pdb)' echo "Remove them or use git commit --no-verify to bypass." exit 1 fi # 3. Check for large files (>1MB) LARGE_FILES=$(git diff --cached --name-only --diff-filter=ACM | while read f; do size=$(wc -c < "$f" 2>/dev/null || echo 0) if [ "$size" -gt 1048576 ]; then echo "$f ($((size/1024))KB)"; fi done) if [ -n "$LARGE_FILES" ]; then echo "ā Large files detected:" echo "$LARGE_FILES" exit 1 fi # 4. Check for secrets patterns if git diff --cached --diff-filter=ACM | grep -nEi '(AKIA[0-9A-Z]{16}|sk-[a-zA-Z0-9]{48}|ghp_[a-zA-Z0-9]{36}|password\s*=\s*["\x27][^"\x27]+["\x27])' > /dev/null 2>&1; then echo "šØ Potential secrets detected in staged changes! Review before committing." exit 1 fi echo "ā All pre-commit checks passed"
Share Custom Hooks via core.hooksPath
# In your repo, set a shared hooks directory git config core.hooksPath .githooks # Add to project setup docs or Makefile # Makefile setup: git config core.hooksPath .githooks chmod +x .githooks/*
CI Integration
Hooks are a first line of defense, but CI is the source of truth.
Run pre-commit in CI (GitHub Actions)
# .github/workflows/lint.yml name: Lint on: [push, pull_request] jobs: pre-commit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '3.12' - uses: pre-commit/action@v3.0.1
Run lint-staged in CI (Validation Only)
# Validate that lint-staged would pass (catch bypassed hooks) name: Lint Check on: [pull_request] jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - run: npm ci - run: npx eslint . --max-warnings=0 - run: npx prettier --check .
Common Pitfalls & Fixes
Hooks Not Running
| Symptom | Cause | Fix |
|---|---|---|
| Hooks silently skipped | Not installed in .git/hooks/ | Run npx husky init or pre-commit install |
| "Permission denied" | Hook file not executable | chmod +x .husky/pre-commit |
| Hooks run but wrong ones | Stale hooks from old setup | Delete .git/hooks/ contents, reinstall |
| Works locally, fails in CI | Different Node/Python versions | Pin versions in CI config |
Performance Issues
// ā Slow: runs on ALL files every commit { "scripts": { "precommit": "eslint src/ && prettier --write src/" } } // ā Fast: lint-staged runs ONLY on staged files { "lint-staged": { "*.{js,ts}": ["eslint --fix", "prettier --write"] } }
Bypassing Hooks (When Needed)
# Skip all hooks for a single commit git commit --no-verify -m "wip: quick save" # Skip pre-push only git push --no-verify # Skip specific pre-commit hooks SKIP=eslint git commit -m "fix: update config"
Warning: Bypassing hooks should be rare. If your team frequently bypasses, the hooks are too slow or too strict ā fix them.
Migration Guide
Husky v4 ā v9 Migration
# 1. Remove old Husky npm uninstall husky rm -rf .husky # 2. Remove old config from package.json # Delete "husky": { "hooks": { ... } } section # 3. Install fresh npm install --save-dev husky npx husky init # 4. Recreate hooks echo "npx lint-staged" > .husky/pre-commit echo "npx --no -- commitlint --edit \$1" > .husky/commit-msg # 5. Clean up ā old Husky used package.json config, # new Husky uses .husky/ directory with plain scripts
Adopting Hooks on an Existing Project
# Step 1: Start with formatting only (low friction) # lint-staged config: { "*.{js,ts}": ["prettier --write"] } # Step 2: Add linting after team adjusts (1-2 weeks later) { "*.{js,ts}": ["eslint --fix", "prettier --write"] } # Step 3: Add commit message linting # Step 4: Add pre-push test runner # Gradual adoption prevents team resistance
Key Principles
- Staged files only ā Never lint the entire codebase on every commit
- Auto-fix when possible ā
--fixflags reduce developer friction - Fast hooks ā Pre-commit should complete in < 5 seconds
- Fail loud ā Clear error messages with actionable fixes
- Team-shared ā Use Husky or
core.hooksPathso hooks are version-controlled - CI as backup ā Hooks are convenience; CI is the enforcer
- Gradual adoption ā Start with formatting, add linting, then testing
Related Skills
@codebase-audit-pre-push- Deep audit before GitHub push@verification-before-completion- Verification before claiming work is done@bash-pro- Advanced shell scripting for custom hooks@github-actions-templates- CI/CD workflow templates