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
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.
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 directoryset -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" ]; thenecho"❌ Direct commits to $BRANCH are not allowed. Use a feature branch."exit 1
fi# 2. Check for debugging artifactsif git diff --cached --diff-filter=ACM | grep -nE '(console\.log|debugger|binding\.pry|import pdb)' > /dev/null 2>&1; thenecho"⚠️ 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 | whileread f; do
size=$(wc -c < "$f" 2>/dev/null || echo 0)
if [ "$size" -gt 1048576 ]; thenecho"$f ($((size/1024))KB)"; fidone)
if [ -n "$LARGE_FILES" ]; thenecho"❌ Large files detected:"echo"$LARGE_FILES"exit 1
fi# 4. Check for secrets patternsif 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; thenecho"🚨 Potential secrets detected in staged changes! Review before committing."exit 1
fiecho"✅ 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.
# Validate that lint-staged would pass (catch bypassed hooks)name:LintCheckon: [pull_request]
jobs:lint:runs-on:ubuntu-lateststeps:-uses:actions/checkout@v4-uses:actions/setup-node@v4with:node-version:20-run:npmci-run:npxeslint.--max-warnings=0-run:npxprettier--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 hooksecho"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 — --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