Git Hooks for Automation: Enforce Quality Before It Hits the Repo

Git hooks run scripts at key points in your workflow. Use them to catch problems before they become pull requests. Hook Basics Hooks live in .git/hooks/. Each hook is an executable script named after the event. 1 2 3 4 5 6 # List available hooks ls .git/hooks/*.sample # Make a hook active cp .git/hooks/pre-commit.sample .git/hooks/pre-commit chmod +x .git/hooks/pre-commit Common Hooks Hook When Use Case pre-commit Before commit created Lint, format, test prepare-commit-msg After default message Add ticket numbers commit-msg After message entered Validate format pre-push Before push Run full tests post-merge After merge Install dependencies post-checkout After checkout Environment setup Pre-commit Hook Basic Linting 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #!/bin/bash # .git/hooks/pre-commit echo "Running pre-commit checks..." # Get staged files STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM) # Python files PYTHON_FILES=$(echo "$STAGED_FILES" | grep '\.py$') if [ -n "$PYTHON_FILES" ]; then echo "Linting Python files..." ruff check $PYTHON_FILES || exit 1 black --check $PYTHON_FILES || exit 1 fi # JavaScript files JS_FILES=$(echo "$STAGED_FILES" | grep -E '\.(js|ts)$') if [ -n "$JS_FILES" ]; then echo "Linting JavaScript files..." eslint $JS_FILES || exit 1 fi echo "All checks passed!" Prevent Debug Statements 1 2 3 4 5 6 7 8 9 #!/bin/bash # .git/hooks/pre-commit # Check for debug statements if git diff --cached | grep -E '(console\.log|debugger|import pdb|breakpoint\(\))'; then echo "ERROR: Debug statements found!" echo "Remove console.log, debugger, pdb, or breakpoint() before committing." exit 1 fi Check for Secrets 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #!/bin/bash # .git/hooks/pre-commit # Patterns that might be secrets PATTERNS=( 'password\s*=\s*["\047][^"\047]+' 'api_key\s*=\s*["\047][^"\047]+' 'secret\s*=\s*["\047][^"\047]+' 'AWS_SECRET_ACCESS_KEY' 'PRIVATE KEY' ) for pattern in "${PATTERNS[@]}"; do if git diff --cached | grep -iE "$pattern"; then echo "ERROR: Possible secret detected!" echo "Pattern: $pattern" exit 1 fi done Commit Message Hook Enforce Conventional Commits 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #!/bin/bash # .git/hooks/commit-msg COMMIT_MSG_FILE=$1 COMMIT_MSG=$(cat "$COMMIT_MSG_FILE") # Conventional commit pattern PATTERN="^(feat|fix|docs|style|refactor|test|chore)(\(.+\))?: .{1,50}" if ! echo "$COMMIT_MSG" | grep -qE "$PATTERN"; then echo "ERROR: Invalid commit message format!" echo "" echo "Expected: <type>(<scope>): <description>" echo "Types: feat, fix, docs, style, refactor, test, chore" echo "" echo "Examples:" echo " feat(auth): add OAuth2 support" echo " fix: resolve memory leak in worker" echo "" exit 1 fi Add Ticket Number 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #!/bin/bash # .git/hooks/prepare-commit-msg COMMIT_MSG_FILE=$1 BRANCH_NAME=$(git symbolic-ref --short HEAD) # Extract ticket number from branch (e.g., feature/JIRA-123-description) TICKET=$(echo "$BRANCH_NAME" | grep -oE '[A-Z]+-[0-9]+') if [ -n "$TICKET" ]; then # Prepend ticket number if not already present if ! grep -q "$TICKET" "$COMMIT_MSG_FILE"; then sed -i.bak "1s/^/[$TICKET] /" "$COMMIT_MSG_FILE" fi fi Pre-push Hook Run Tests Before Push 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #!/bin/bash # .git/hooks/pre-push echo "Running tests before push..." # Run test suite npm test if [ $? -ne 0 ]; then echo "ERROR: Tests failed! Push aborted." exit 1 fi # Check coverage threshold npm run coverage COVERAGE=$(cat coverage/coverage-summary.json | jq '.total.lines.pct') if (( $(echo "$COVERAGE < 80" | bc -l) )); then echo "ERROR: Coverage below 80%! Current: $COVERAGE%" exit 1 fi echo "All checks passed. Pushing..." Prevent Force Push to Main 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #!/bin/bash # .git/hooks/pre-push PROTECTED_BRANCHES="main master" CURRENT_BRANCH=$(git symbolic-ref --short HEAD) # Check if force pushing while read local_ref local_sha remote_ref remote_sha; do if echo "$PROTECTED_BRANCHES" | grep -qw "$CURRENT_BRANCH"; then if [ "$remote_sha" != "0000000000000000000000000000000000000000" ]; then # Check if this is a force push if ! git merge-base --is-ancestor "$remote_sha" "$local_sha" 2>/dev/null; then echo "ERROR: Force push to $CURRENT_BRANCH is not allowed!" exit 1 fi fi fi done Post-merge Hook Auto-install Dependencies 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #!/bin/bash # .git/hooks/post-merge CHANGED_FILES=$(git diff-tree -r --name-only ORIG_HEAD HEAD) # Check if package.json changed if echo "$CHANGED_FILES" | grep -q "package.json"; then echo "package.json changed, running npm install..." npm install fi # Check if requirements.txt changed if echo "$CHANGED_FILES" | grep -q "requirements.txt"; then echo "requirements.txt changed, running pip install..." pip install -r requirements.txt fi # Check if migrations added if echo "$CHANGED_FILES" | grep -q "migrations/"; then echo "New migrations detected!" echo "Run: python manage.py migrate" fi Using pre-commit Framework The pre-commit framework manages hooks declaratively. ...

February 25, 2026 · 6 min · 1136 words · Rob Washington

Testing Strategies: The Pyramid That Actually Works

Everyone agrees testing is important. Few agree on how much, what kind, and where to focus. The testing pyramid provides a framework: many fast unit tests, fewer integration tests, even fewer end-to-end tests. But the pyramid only works if you understand why each layer exists. The Pyramid E I U 2 n n E t i e t T g e r T s a e t t s s i t o s ( n f ( e T m w e a , s n t y s s , l o ( f w s a , o s m t e e , x , p c e m h n e e s d a i i p v u ) e m ) s p e e d ) Unit tests: Test individual functions/classes in isolation Integration tests: Test components working together E2E tests: Test the full system like a user would ...

February 24, 2026 · 7 min · 1367 words · Rob Washington