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
Hook | When | Purpose | Can Block |
---|---|---|---|
pre-commit | Before commit creation | Code quality, tests | Yes |
prepare-commit-msg | Before commit message editor | Auto-generate messages | No |
commit-msg | After commit message entered | Message validation | Yes |
post-commit | After commit created | Notifications, cleanup | No |
pre-push | Before pushing changes | Final validation | Yes |
post-checkout | After checkout/switch | Setup, notifications | No |
post-merge | After successful merge | Cleanup, rebuilds | No |
Server-Side Hooks
Hook | When | Purpose | Can Block |
---|---|---|---|
pre-receive | Before any ref updates | Global policies | Yes |
update | Before each ref update | Per-ref policies | Yes |
post-receive | After all ref updates | Notifications, deployment | No |
post-update | After each ref update | Repository maintenance | No |
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.