Git - Hooks

Advanced

Git hooks are scripts that run automatically at specific points in the Git workflow. They enable powerful automation, quality gates, and custom behaviors that can transform your development process.

What are Git Hooks?

Hooks are executable scripts that Git calls at specific moments:

Git Action โ†’ Trigger โ†’ Hook Script โ†’ Result
   โ†“              โ†“           โ†“         โ†“
git commit โ†’ pre-commit โ†’ validate โ†’ โœ…/โŒ

Key characteristics:

  • Automatic: Run without manual intervention
  • Customizable: Any executable script (bash, Python, Node.js, etc.)
  • Local or Server: Client-side or repository-side execution
  • Blocking: Can prevent Git operations if they fail

Hook Types

Client-Side Hooks

HookWhenPurposeCan Block
pre-commitBefore commit creationCode quality, testsYes
prepare-commit-msgBefore commit message editorAuto-generate messagesNo
commit-msgAfter commit message enteredMessage validationYes
post-commitAfter commit createdNotifications, cleanupNo
pre-pushBefore pushing changesFinal validationYes
post-checkoutAfter checkout/switchSetup, notificationsNo
post-mergeAfter successful mergeCleanup, rebuildsNo

Server-Side Hooks

HookWhenPurposeCan Block
pre-receiveBefore any ref updatesGlobal policiesYes
updateBefore each ref updatePer-ref policiesYes
post-receiveAfter all ref updatesNotifications, deploymentNo
post-updateAfter each ref updateRepository maintenanceNo

Hook Location and Setup

Hook Directory

# Default hook location
.git/hooks/

# List available hook templates
ls -la .git/hooks/

# Example hooks (templates with .sample extension)
applypatch-msg.sample
commit-msg.sample
pre-commit.sample
pre-push.sample

Creating Your First Hook

Create pre-commit hook:
# Create the hook file
nano .git/hooks/pre-commit

# Make it executable
chmod +x .git/hooks/pre-commit
Simple pre-commit hook example:
#!/bin/sh
# .git/hooks/pre-commit

echo "Running pre-commit checks..."

# Check for TODO comments
if grep -r "TODO" --include="*.js" --include="*.py" .; then
    echo "Error: TODO comments found. Please resolve before committing."
    exit 1
fi

echo "Pre-commit checks passed!"
exit 0

Practical Hook Examples

Pre-Commit Quality Gates

Comprehensive pre-commit hook:
#!/bin/bash
# .git/hooks/pre-commit

set -e  # Exit on any error

echo "๐Ÿ” Running pre-commit checks..."

# Check for merge conflict markers
if grep -r "<<<<<<< HEAD" --include="*.js" --include="*.py" --include="*.css" .; then
    echo "โŒ Merge conflict markers found!"
    exit 1
fi

# Run linter
echo "๐Ÿงน Running linter..."
if command -v eslint &> /dev/null; then
    eslint src/ || exit 1
fi

# Run tests
echo "๐Ÿงช Running tests..."
if command -v npm &> /dev/null; then
    npm test || exit 1
fi

# Check file sizes
echo "๐Ÿ“ Checking file sizes..."
large_files=$(find . -name "*.js" -o -name "*.css" | xargs ls -la | awk '$5 > 1000000 {print $9 " (" $5 " bytes)"}')
if [ -n "$large_files" ]; then
    echo "โš ๏ธ  Large files detected:"
    echo "$large_files"
    echo "Consider optimizing or using Git LFS"
fi

echo "โœ… All pre-commit checks passed!"

Commit Message Validation

commit-msg hook for conventional commits:
#!/bin/bash
# .git/hooks/commit-msg

commit_message_file=$1
commit_message=$(cat "$commit_message_file")

# Define conventional commit pattern
pattern="^(feat|fix|docs|style|refactor|test|chore)(\(.+\))?: .{1,50}"

if [[ ! $commit_message =~ $pattern ]]; then
    echo "โŒ Invalid commit message format!"
    echo ""
    echo "Commit message must follow conventional commits format:"
    echo "type(scope): description"
    echo ""
    echo "Types: feat, fix, docs, style, refactor, test, chore"
    echo "Example: feat(auth): add login functionality"
    echo ""
    echo "Your message: $commit_message"
    exit 1
fi

echo "โœ… Commit message format is valid"

Pre-Push Security Check

pre-push hook for security:
#!/bin/bash
# .git/hooks/pre-push

echo "๐Ÿ”’ Running security checks..."

# Check for common secrets patterns
secrets_found=false

# API keys
if grep -r "api[_-]key" --include="*.js" --include="*.py" --include="*.env" .; then
    echo "โš ๏ธ  Potential API key found!"
    secrets_found=true
fi

# Passwords
if grep -r "password\s*=" --include="*.js" --include="*.py" .; then
    echo "โš ๏ธ  Potential hardcoded password found!"
    secrets_found=true
fi

# Private keys
if grep -r "BEGIN.*PRIVATE KEY" --include="*.pem" --include="*.key" .; then
    echo "โš ๏ธ  Private key file found!"
    secrets_found=true
fi

if [ "$secrets_found" = true ]; then
    echo "โŒ Security issues detected. Push aborted."
    echo "Please remove sensitive data before pushing."
    exit 1
fi

echo "โœ… Security checks passed!"

Advanced Hook Implementations

Node.js/JavaScript Hook

package.json scripts integration:
#!/usr/bin/env node
// .git/hooks/pre-commit

const { execSync } = require('child_process');

console.log('๐Ÿ” Running pre-commit checks...');

try {
    // Run linting
    console.log('๐Ÿงน Linting...');
    execSync('npm run lint', { stdio: 'inherit' });
    
    // Run tests
    console.log('๐Ÿงช Testing...');
    execSync('npm run test:unit', { stdio: 'inherit' });
    
    // Type checking
    console.log('๐Ÿ” Type checking...');
    execSync('npm run type-check', { stdio: 'inherit' });
    
    console.log('โœ… All checks passed!');
    process.exit(0);
} catch (error) {
    console.log('โŒ Pre-commit checks failed!');
    process.exit(1);
}

Python Hook with Rich Output

Python pre-commit hook:
#!/usr/bin/env python3
# .git/hooks/pre-commit

import subprocess
import sys
import os

def run_command(command, description):
    """Run a command and handle its output."""
    print(f"๐Ÿ” {description}...")
    try:
        result = subprocess.run(
            command.split(),
            capture_output=True,
            text=True,
            check=True
        )
        print(f"โœ… {description} passed")
        return True
    except subprocess.CalledProcessError as e:
        print(f"โŒ {description} failed:")
        print(e.stdout)
        print(e.stderr)
        return False

def main():
    print("๐Ÿš€ Starting pre-commit checks...")
    
    checks = [
        ("python -m flake8 .", "Linting with flake8"),
        ("python -m pytest tests/", "Running tests"),
        ("python -m mypy src/", "Type checking"),
    ]
    
    all_passed = True
    for command, description in checks:
        if not run_command(command, description):
            all_passed = False
    
    if all_passed:
        print("๐ŸŽ‰ All pre-commit checks passed!")
        sys.exit(0)
    else:
        print("๐Ÿ’ฅ Some checks failed. Commit aborted.")
        sys.exit(1)

if __name__ == "__main__":
    main()

Server-Side Hook Examples

Branch Protection Hook

pre-receive hook for branch protection:
#!/bin/bash
# hooks/pre-receive (on server)

protected_branches="main master production"

while read oldrev newrev refname; do
    branch=$(echo $refname | sed 's|refs/heads/||')
    
    # Check if branch is protected
    for protected in $protected_branches; do
        if [ "$branch" = "$protected" ]; then
            echo "โŒ Direct push to $branch is not allowed!"
            echo "Please use pull requests for protected branches."
            exit 1
        fi
    done
    
    # Check for force push
    if [ "$oldrev" != "0000000000000000000000000000000000000000" ]; then
        if ! git merge-base --is-ancestor $oldrev $newrev; then
            echo "โŒ Force push detected to $branch!"
            echo "Force pushes are not allowed."
            exit 1
        fi
    fi
done

echo "โœ… Push approved"

Deployment Hook

post-receive hook for deployment:
#!/bin/bash
# hooks/post-receive (on server)

while read oldrev newrev refname; do
    branch=$(echo $refname | sed 's|refs/heads/||')
    
    if [ "$branch" = "main" ]; then
        echo "๐Ÿš€ Deploying to production..."
        
        # Update working directory
        git --git-dir=/var/repo/project.git --work-tree=/var/www/html checkout -f
        
        # Install dependencies
        cd /var/www/html
        npm ci --production
        
        # Build application
        npm run build
        
        # Restart services
        systemctl restart nginx
        systemctl restart nodejs-app
        
        # Send notification
        curl -X POST -H 'Content-type: application/json' \
            --data '{"text":"๐Ÿš€ Production deployment completed"}' \
            $SLACK_WEBHOOK_URL
            
        echo "โœ… Deployment completed!"
    fi
done

Hook Management Tools

Sharing Hooks with Team

Setup script for team hooks:
#!/bin/bash
# scripts/setup-hooks.sh

HOOKS_DIR="$(git rev-parse --git-dir)/hooks"
PROJECT_HOOKS_DIR="$(git rev-parse --show-toplevel)/git-hooks"

# Create project hooks directory if it doesn't exist
mkdir -p "$PROJECT_HOOKS_DIR"

# Copy hooks from project to .git/hooks
for hook in "$PROJECT_HOOKS_DIR"/*; do
    if [ -f "$hook" ]; then
        hook_name=$(basename "$hook")
        cp "$hook" "$HOOKS_DIR/$hook_name"
        chmod +x "$HOOKS_DIR/$hook_name"
        echo "โœ… Installed $hook_name hook"
    fi
done

echo "๐ŸŽ‰ All hooks installed!"

Popular Hook Management Tools

# Husky (Node.js projects)
npm install --save-dev husky
npx husky init
echo "npm test" > .husky/pre-commit

# pre-commit (Python-based)
pip install pre-commit
# Create .pre-commit-config.yaml
pre-commit install

# lefthook (Go-based)
gem install lefthook
lefthook install

Hook Configuration Examples

Husky Configuration

package.json:
{
  "scripts": {
    "prepare": "husky",
    "lint": "eslint src/",
    "test": "jest",
    "type-check": "tsc --noEmit"
  },
  "devDependencies": {
    "husky": "^9.0.0",
    "lint-staged": "^15.0.0"
  },
  "lint-staged": {
    "*.{js,jsx,ts,tsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{md,json}": [
      "prettier --write"
    ]
  }
}
.husky/pre-commit:
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npx lint-staged

Pre-commit Framework Config

.pre-commit-config.yaml:
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.4.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
      - id: check-added-large-files
      - id: check-merge-conflict
  
  - repo: https://github.com/psf/black
    rev: 23.7.0
    hooks:
      - id: black
        language_version: python3
  
  - repo: https://github.com/pycqa/flake8
    rev: 6.0.0
    hooks:
      - id: flake8

Debugging Hooks

Hook Troubleshooting

# Test hook manually
.git/hooks/pre-commit

# Debug hook execution
git -c core.hooksPath=.git/hooks commit -m "test"

# Skip hooks temporarily
git commit --no-verify -m "emergency commit"

# Check hook permissions
ls -la .git/hooks/

Hook Logging

Add logging to hooks:
#!/bin/bash
# Add to beginning of hook

LOG_FILE=".git/hooks.log"
echo "$(date): pre-commit hook started" >> $LOG_FILE

# Your hook logic here...

echo "$(date): pre-commit hook completed" >> $LOG_FILE

Best Practices

Hook Development Guidelines

  • โœ… Keep hooks fast (< 30 seconds)
  • โœ… Provide clear error messages
  • โœ… Make hooks skippable for emergencies
  • โœ… Test hooks thoroughly
  • โœ… Use proper exit codes (0 = success, 1 = failure)
  • โœ… Handle edge cases gracefully
  • โœ… Document hook requirements

Team Adoption Strategy

  • โœ… Start with non-blocking hooks (post-commit)
  • โœ… Gradually introduce quality gates
  • โœ… Provide easy setup scripts
  • โœ… Train team on hook usage
  • โœ… Monitor hook performance
  • โœ… Regular hook maintenance

Summary

You now understand Git hooks:

  • โœ… Client-side vs server-side hooks
  • โœ… Common hook types and their purposes
  • โœ… Practical implementations for quality gates
  • โœ… Advanced hooks with multiple languages
  • โœ… Team hook management and sharing
  • โœ… Popular hook management tools
  • โœ… Debugging and troubleshooting hooks

Next Steps

Now that you understand hooks, let's explore Git internals and how Git works under the hood:

โ†’ Git - Internals

Practice Tip: Start with simple hooks like pre-commit linting, then gradually add more sophisticated automation as your team's needs grow.