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)
bash
1# Create a pre-commit hook manually
2cat > .git/hooks/pre-commit << 'EOF'
3#!/bin/sh
4set -e
5
6# Run linter on staged files only
7STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(js|ts|jsx|tsx)$' || true)
8
9if [ -n "$STAGED_FILES" ]; then
10 echo "🔍 Linting staged files..."
11 echo "$STAGED_FILES" | xargs npx eslint --fix
12 echo "$STAGED_FILES" | xargs git add # Re-stage after fixes
13fi
14EOF
15chmod +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+)
bash
1# Install
2npm install --save-dev husky lint-staged
3
4# Initialize Husky (creates .husky/ directory)
5npx husky init
6
7# The init command creates a pre-commit hook — edit it:
8echo "npx lint-staged" > .husky/pre-commit
Configure lint-staged in package.json
json
1{
2 "lint-staged": {
3 "*.{js,jsx,ts,tsx}": [
4 "eslint --fix --max-warnings=0",
5 "prettier --write"
6 ],
7 "*.{css,scss}": [
8 "prettier --write",
9 "stylelint --fix"
10 ],
11 "*.{json,md,yml,yaml}": [
12 "prettier --write"
13 ]
14 }
15}
Add Commit Message Linting
bash
1# Install commitlint
2npm install --save-dev @commitlint/cli @commitlint/config-conventional
3
4# Create commitlint config
5cat > commitlint.config.js << 'EOF'
6module.exports = {
7 extends: ['@commitlint/config-conventional'],
8 rules: {
9 'type-enum': [2, 'always', [
10 'feat', 'fix', 'docs', 'style', 'refactor',
11 'perf', 'test', 'build', 'ci', 'chore', 'revert'
12 ]],
13 'subject-max-length': [2, 'always', 72],
14 'body-max-line-length': [2, 'always', 100]
15 }
16};
17EOF
18
19# Add commit-msg hook
20echo "npx --no -- commitlint --edit \$1" > .husky/commit-msg
Add Pre-Push Hook
bash
1# Run tests before pushing
2echo "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
bash
1# Install (Python required)
2pip install pre-commit
3
4# Create config
5cat > .pre-commit-config.yaml << 'EOF'
6repos:
7 # Built-in checks
8 - repo: https://github.com/pre-commit/pre-commit-hooks
9 rev: v4.6.0
10 hooks:
11 - id: trailing-whitespace
12 - id: end-of-file-fixer
13 - id: check-yaml
14 - id: check-json
15 - id: check-added-large-files
16 args: ['--maxkb=500']
17 - id: check-merge-conflict
18 - id: detect-private-key
19
20 # Python formatting
21 - repo: https://github.com/psf/black
22 rev: 24.4.2
23 hooks:
24 - id: black
25
26 # Python linting
27 - repo: https://github.com/astral-sh/ruff-pre-commit
28 rev: v0.4.4
29 hooks:
30 - id: ruff
31 args: ['--fix']
32 - id: ruff-format
33
34 # Shell script linting
35 - repo: https://github.com/shellcheck-py/shellcheck-py
36 rev: v0.10.0.1
37 hooks:
38 - id: shellcheck
39
40 # Commit message format
41 - repo: https://github.com/compilerla/conventional-pre-commit
42 rev: v3.2.0
43 hooks:
44 - id: conventional-pre-commit
45 stages: [commit-msg]
46EOF
47
48# Install hooks into .git/hooks/
49pre-commit install
50pre-commit install --hook-type commit-msg
51
52# Run against all files (first time)
53pre-commit run --all-files
Key Commands
bash
1pre-commit install # Install hooks
2pre-commit run --all-files # Run on everything (CI or first setup)
3pre-commit autoupdate # Update hook versions
4pre-commit run <hook-id> # Run a specific hook
5pre-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
bash
1#!/bin/sh
2# .githooks/pre-commit — Team-shared hooks directory
3set -e
4
5echo "=== Pre-Commit Checks ==="
6
7# 1. Prevent commits to main/master
8BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || echo "detached")
9if [ "$BRANCH" = "main" ] || [ "$BRANCH" = "master" ]; then
10 echo "❌ Direct commits to $BRANCH are not allowed. Use a feature branch."
11 exit 1
12fi
13
14# 2. Check for debugging artifacts
15if git diff --cached --diff-filter=ACM | grep -nE '(console\.log|debugger|binding\.pry|import pdb)' > /dev/null 2>&1; then
16 echo "⚠️ Debug statements found in staged files:"
17 git diff --cached --diff-filter=ACM | grep -nE '(console\.log|debugger|binding\.pry|import pdb)'
18 echo "Remove them or use git commit --no-verify to bypass."
19 exit 1
20fi
21
22# 3. Check for large files (>1MB)
23LARGE_FILES=$(git diff --cached --name-only --diff-filter=ACM | while read f; do
24 size=$(wc -c < "$f" 2>/dev/null || echo 0)
25 if [ "$size" -gt 1048576 ]; then echo "$f ($((size/1024))KB)"; fi
26done)
27if [ -n "$LARGE_FILES" ]; then
28 echo "❌ Large files detected:"
29 echo "$LARGE_FILES"
30 exit 1
31fi
32
33# 4. Check for secrets patterns
34if 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
35 echo "🚨 Potential secrets detected in staged changes! Review before committing."
36 exit 1
37fi
38
39echo "✅ All pre-commit checks passed"
Share Custom Hooks via core.hooksPath
bash
1# In your repo, set a shared hooks directory
2git config core.hooksPath .githooks
3
4# Add to project setup docs or Makefile
5# Makefile
6setup:
7 git config core.hooksPath .githooks
8 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)
yaml
1# .github/workflows/lint.yml
2name: Lint
3on: [push, pull_request]
4jobs:
5 pre-commit:
6 runs-on: ubuntu-latest
7 steps:
8 - uses: actions/checkout@v4
9 - uses: actions/setup-python@v5
10 with:
11 python-version: '3.12'
12 - uses: pre-commit/action@v3.0.1
Run lint-staged in CI (Validation Only)
yaml
1# Validate that lint-staged would pass (catch bypassed hooks)
2name: Lint Check
3on: [pull_request]
4jobs:
5 lint:
6 runs-on: ubuntu-latest
7 steps:
8 - uses: actions/checkout@v4
9 - uses: actions/setup-node@v4
10 with:
11 node-version: 20
12 - run: npm ci
13 - run: npx eslint . --max-warnings=0
14 - 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
json
1// ❌ Slow: runs on ALL files every commit
2{
3 "scripts": {
4 "precommit": "eslint src/ && prettier --write src/"
5 }
6}
7
8// ✅ Fast: lint-staged runs ONLY on staged files
9{
10 "lint-staged": {
11 "*.{js,ts}": ["eslint --fix", "prettier --write"]
12 }
13}
Bypassing Hooks (When Needed)
bash
1# Skip all hooks for a single commit
2git commit --no-verify -m "wip: quick save"
3
4# Skip pre-push only
5git push --no-verify
6
7# Skip specific pre-commit hooks
8SKIP=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
bash
1# 1. Remove old Husky
2npm uninstall husky
3rm -rf .husky
4
5# 2. Remove old config from package.json
6# Delete "husky": { "hooks": { ... } } section
7
8# 3. Install fresh
9npm install --save-dev husky
10npx husky init
11
12# 4. Recreate hooks
13echo "npx lint-staged" > .husky/pre-commit
14echo "npx --no -- commitlint --edit \$1" > .husky/commit-msg
15
16# 5. Clean up — old Husky used package.json config,
17# new Husky uses .husky/ directory with plain scripts
Adopting Hooks on an Existing Project
bash
1# Step 1: Start with formatting only (low friction)
2# lint-staged config:
3{ "*.{js,ts}": ["prettier --write"] }
4
5# Step 2: Add linting after team adjusts (1-2 weeks later)
6{ "*.{js,ts}": ["eslint --fix", "prettier --write"] }
7
8# Step 3: Add commit message linting
9# Step 4: Add pre-push test runner
10
11# Gradual adoption prevents team resistance
Key Principles
- Staged files only — Never lint the entire codebase on every commit
- Auto-fix when possible —
--fix flags 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.hooksPath so 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