Table Of Contents
- Introduction
- Understanding Git Hooks
- Setting Up Your First Git Hook
- Common Code Quality Checks
- Advanced Hook Implementation
- Using Git Hook Managers
- Best Practices for Git Hooks
- Troubleshooting Common Issues
- Integration with Development Workflows
- FAQ Section
- Conclusion
Introduction
Every development team struggles with maintaining consistent code quality. Code reviews catch many issues, but what if you could prevent problematic code from even being committed? Git hooks provide the perfect solution—automated scripts that run at specific points in your Git workflow.
Git hooks act as your first line of defense against code quality issues. They can automatically format code, run tests, check for security vulnerabilities, and enforce coding standards before changes ever leave a developer's machine. This proactive approach saves time, reduces review cycles, and maintains high code quality standards across your entire team.
In this comprehensive guide, you'll learn how to implement Git hooks that automatically enforce code quality checks, making your development workflow more efficient and your codebase more maintainable.
Understanding Git Hooks
What Are Git Hooks?
Git hooks are scripts that Git executes before or after events such as commit, push, and receive. They're stored in the .git/hooks
directory of every Git repository and can be written in any scripting language—Bash, Python, Node.js, or any executable script.
Types of Git Hooks
Git provides two types of hooks:
Client-Side Hooks:
pre-commit
: Runs before a commit is createdprepare-commit-msg
: Runs before the commit message editor is openedcommit-msg
: Validates the commit messagepost-commit
: Runs after a commit is createdpre-push
: Runs before pushing to a remote repositorypre-rebase
: Runs before a rebase operation
Server-Side Hooks:
pre-receive
: Runs before accepting pushed commitsupdate
: Similar to pre-receive but runs once per branchpost-receive
: Runs after push is complete
How Git Hooks Work
When you initialize a Git repository, Git creates sample hook scripts in .git/hooks/
:
ls .git/hooks/
# applypatch-msg.sample
# commit-msg.sample
# pre-commit.sample
# pre-push.sample
# pre-rebase.sample
# ...
To activate a hook, remove the .sample
extension and ensure the script is executable:
mv .git/hooks/pre-commit.sample .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit
Setting Up Your First Git Hook
Creating a Simple Pre-Commit Hook
Let's create a basic pre-commit hook that checks for debugging statements:
#!/bin/sh
# .git/hooks/pre-commit
# Check for console.log statements in JavaScript files
if git diff --cached --name-only | grep -E '\.js$' | xargs grep -n 'console\.log'; then
echo "Error: Found console.log statements in JavaScript files"
echo "Please remove them before committing"
exit 1
fi
exit 0
Making Hooks Executable
chmod +x .git/hooks/pre-commit
Testing Your Hook
# Add a file with console.log
echo "console.log('debug');" > test.js
git add test.js
git commit -m "Test commit"
# Error: Found console.log statements in JavaScript files
Common Code Quality Checks
Linting and Code Style
ESLint for JavaScript/TypeScript
#!/bin/sh
# .git/hooks/pre-commit
# Run ESLint on staged JavaScript/TypeScript files
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(js|jsx|ts|tsx)$')
if [ -n "$STAGED_FILES" ]; then
echo "Running ESLint..."
npx eslint $STAGED_FILES --fix
# Add fixed files back to staging
git add $STAGED_FILES
# Re-run to check if issues remain
if ! npx eslint $STAGED_FILES; then
echo "ESLint failed. Please fix errors before committing."
exit 1
fi
fi
Python Black Formatter
#!/bin/sh
# .git/hooks/pre-commit
# Format Python files with Black
STAGED_PY_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.py$')
if [ -n "$STAGED_PY_FILES" ]; then
echo "Running Black formatter..."
black $STAGED_PY_FILES
git add $STAGED_PY_FILES
fi
Running Tests
Pre-Push Test Runner
#!/bin/sh
# .git/hooks/pre-push
echo "Running tests before push..."
# Run different test suites based on changed files
if git diff --name-only @{u}... | grep -E '\.(js|ts)$' > /dev/null; then
echo "Running JavaScript tests..."
if ! npm test; then
echo "Tests failed. Push aborted."
exit 1
fi
fi
if git diff --name-only @{u}... | grep '\.py$' > /dev/null; then
echo "Running Python tests..."
if ! python -m pytest; then
echo "Tests failed. Push aborted."
exit 1
fi
fi
echo "All tests passed!"
Security Scanning
Detecting Secrets
#!/bin/sh
# .git/hooks/pre-commit
# Check for potential secrets
PATTERNS=(
'password\s*=\s*["\'][^"\']+["\']'
'api[_-]?key\s*=\s*["\'][^"\']+["\']'
'secret\s*=\s*["\'][^"\']+["\']'
'token\s*=\s*["\'][^"\']+["\']'
'BEGIN RSA PRIVATE KEY'
'BEGIN DSA PRIVATE KEY'
'BEGIN EC PRIVATE KEY'
'BEGIN PGP PRIVATE KEY'
)
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
for pattern in "${PATTERNS[@]}"; do
if echo "$STAGED_FILES" | xargs grep -E -i "$pattern" 2>/dev/null; then
echo "Error: Potential secret detected!"
echo "Please remove sensitive information before committing."
exit 1
fi
done
Commit Message Validation
#!/bin/sh
# .git/hooks/commit-msg
# Enforce conventional commit format
commit_regex='^(feat|fix|docs|style|refactor|test|chore|perf|ci|build|revert)(\(.+\))?: .{1,50}'
if ! grep -qE "$commit_regex" "$1"; then
echo "Invalid commit message format!"
echo "Format: <type>(<scope>): <subject>"
echo "Example: feat(auth): add login functionality"
echo ""
echo "Types: feat|fix|docs|style|refactor|test|chore|perf|ci|build|revert"
exit 1
fi
# Check message length
if [ $(head -1 "$1" | wc -c) -gt 72 ]; then
echo "Commit message too long. Keep under 72 characters."
exit 1
fi
Advanced Hook Implementation
Multi-Language Support Hook
#!/bin/bash
# .git/hooks/pre-commit
# Color output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo -e "${YELLOW}Running pre-commit checks...${NC}"
# Track overall status
PASS=true
# JavaScript/TypeScript checks
JS_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(js|jsx|ts|tsx)$')
if [ -n "$JS_FILES" ]; then
echo -e "${YELLOW}Checking JavaScript/TypeScript files...${NC}"
# ESLint
if ! npx eslint $JS_FILES; then
PASS=false
fi
# Prettier
npx prettier --write $JS_FILES
git add $JS_FILES
fi
# Python checks
PY_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.py$')
if [ -n "$PY_FILES" ]; then
echo -e "${YELLOW}Checking Python files...${NC}"
# Flake8
if ! flake8 $PY_FILES; then
PASS=false
fi
# Black
black $PY_FILES
git add $PY_FILES
fi
# Go checks
GO_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.go$')
if [ -n "$GO_FILES" ]; then
echo -e "${YELLOW}Checking Go files...${NC}"
# gofmt
UNFORMATTED=$(gofmt -l $GO_FILES)
if [ -n "$UNFORMATTED" ]; then
echo "Go files must be formatted with gofmt:"
echo "$UNFORMATTED"
PASS=false
fi
# go vet
if ! go vet ./...; then
PASS=false
fi
fi
# Final status
if [ "$PASS" = true ]; then
echo -e "${GREEN}All checks passed!${NC}"
exit 0
else
echo -e "${RED}Some checks failed. Please fix issues before committing.${NC}"
exit 1
fi
Performance Optimization
#!/bin/bash
# .git/hooks/pre-commit
# Only check modified files
get_staged_files() {
git diff --cached --name-only --diff-filter=ACM | grep -E "$1"
}
# Run checks in parallel
run_parallel_checks() {
local pids=()
# JavaScript lint in background
if [ -n "$(get_staged_files '\.(js|jsx|ts|tsx)$')" ]; then
npx eslint $(get_staged_files '\.(js|jsx|ts|tsx)$') &
pids+=($!)
fi
# Python lint in background
if [ -n "$(get_staged_files '\.py$')" ]; then
flake8 $(get_staged_files '\.py$') &
pids+=($!)
fi
# Wait for all background jobs
local failed=0
for pid in "${pids[@]}"; do
if ! wait $pid; then
failed=1
fi
done
return $failed
}
# Execute checks
if ! run_parallel_checks; then
echo "Pre-commit checks failed!"
exit 1
fi
Using Git Hook Managers
Husky (JavaScript/Node.js)
Installation
npm install --save-dev husky
npx husky install
npm pkg set scripts.prepare="husky install"
Configuration
# Add pre-commit hook
npx husky add .husky/pre-commit "npm run lint"
# Add pre-push hook
npx husky add .husky/pre-push "npm test"
Package.json Integration
{
"scripts": {
"prepare": "husky install",
"lint": "eslint . --fix",
"test": "jest"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"],
"*.{json,md}": ["prettier --write"]
}
}
Pre-commit Framework (Python)
Installation
pip install pre-commit
Configuration (.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.1.0
hooks:
- id: black
- repo: https://github.com/pycqa/flake8
rev: 6.0.0
hooks:
- id: flake8
- repo: https://github.com/pre-commit/mirrors-eslint
rev: v8.36.0
hooks:
- id: eslint
files: \.(js|jsx|ts|tsx)$
Setup
# Install hooks
pre-commit install
# Run on all files
pre-commit run --all-files
# Update hooks
pre-commit autoupdate
Lefthook (Multi-language)
Configuration (lefthook.yml)
pre-commit:
parallel: true
commands:
eslint:
glob: "*.{js,jsx,ts,tsx}"
run: npx eslint {staged_files}
prettier:
glob: "*.{js,jsx,ts,tsx,json,md}"
run: npx prettier --write {staged_files} && git add {staged_files}
python-black:
glob: "*.py"
run: black {staged_files} && git add {staged_files}
go-fmt:
glob: "*.go"
run: gofmt -w {staged_files} && git add {staged_files}
pre-push:
commands:
test-js:
run: npm test
test-python:
run: python -m pytest
audit:
run: npm audit
commit-msg:
commands:
commitlint:
run: npx commitlint --edit
Best Practices for Git Hooks
Keep Hooks Fast
#!/bin/bash
# Set timeout for long-running checks
timeout_check() {
timeout 30s "$@"
local status=$?
if [ $status -eq 124 ]; then
echo "Check timed out after 30 seconds"
return 1
fi
return $status
}
# Use timeout for potentially slow operations
timeout_check npm run lint
timeout_check npm test
Make Hooks Configurable
#!/bin/bash
# Allow developers to skip hooks when needed
# Check for skip environment variable
if [ "$SKIP_HOOKS" = "1" ]; then
echo "Skipping pre-commit hooks (SKIP_HOOKS=1)"
exit 0
fi
# Check for --no-verify flag
if [ "$GIT_SKIP_HOOKS" = "1" ]; then
exit 0
fi
# Run normal hooks...
Provide Clear Error Messages
#!/bin/bash
# Function for formatted output
print_error() {
echo -e "\033[0;31m✗ $1\033[0m"
}
print_success() {
echo -e "\033[0;32m✓ $1\033[0m"
}
print_info() {
echo -e "\033[0;34mℹ $1\033[0m"
}
# Example usage
if ! npm run lint; then
print_error "Linting failed"
print_info "Run 'npm run lint:fix' to automatically fix issues"
print_info "Or commit with --no-verify to skip hooks"
exit 1
fi
print_success "All checks passed!"
Version Control Your Hooks
# Create hooks directory in your project
mkdir .githooks
# Copy hooks
cp .git/hooks/pre-commit .githooks/
# Create setup script
cat > setup-hooks.sh << 'EOF'
#!/bin/bash
echo "Setting up Git hooks..."
# Create symlinks
ln -sf ../../.githooks/pre-commit .git/hooks/pre-commit
ln -sf ../../.githooks/pre-push .git/hooks/pre-push
chmod +x .git/hooks/*
echo "Git hooks installed successfully!"
EOF
chmod +x setup-hooks.sh
Troubleshooting Common Issues
Hook Not Executing
# Check if hook is executable
ls -la .git/hooks/pre-commit
# Make executable
chmod +x .git/hooks/pre-commit
# Check shebang line
head -1 .git/hooks/pre-commit
# Should be: #!/bin/sh or #!/bin/bash
Hook Failing on CI/CD
# Detect CI environment
if [ -n "$CI" ] || [ -n "$CONTINUOUS_INTEGRATION" ]; then
echo "Running in CI environment, skipping interactive checks"
exit 0
fi
Windows Compatibility
#!/bin/sh
# Handle Windows line endings
# Convert CRLF to LF for staged files
if [ "$OS" = "Windows_NT" ]; then
git config core.autocrlf true
fi
# Use cross-platform commands
# Instead of: grep -E
# Use: git grep -E
Integration with Development Workflows
IDE Integration
VS Code Settings
{
"git.enableCommitSigning": true,
"git.confirmSync": false,
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
}
JetBrains IDE Configuration
<!-- .idea/git_hooks.xml -->
<component name="GitHooks">
<option name="preCommitHook" value=".githooks/pre-commit" />
<option name="prePushHook" value=".githooks/pre-push" />
</component>
CI/CD Pipeline Integration
# GitHub Actions
name: Code Quality
on: [push, pull_request]
jobs:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run pre-commit hooks
uses: pre-commit/action@v3.0.0
- name: Run tests
run: |
npm install
npm test
FAQ Section
Q: Can I skip Git hooks temporarily?
Yes, use git commit --no-verify
or git push --no-verify
to skip hooks for a single operation. You can also set SKIP_HOOKS=1
environment variable if your hooks support it.
Q: How do I share Git hooks with my team?
Store hooks in a directory like .githooks/
in your repository and provide a setup script. Alternatively, use hook managers like Husky or pre-commit that automatically install hooks.
Q: Do Git hooks work with GUI Git clients?
Most GUI clients respect Git hooks, but some may not show hook output clearly. Test your hooks with the specific GUI tools your team uses.
Q: Can hooks modify staged files?
Yes, hooks can modify files, but you must re-stage them using git add
within the hook script for changes to be included in the commit.
Q: What happens if a hook script fails?
If a pre-commit or pre-push hook exits with a non-zero status, Git cancels the operation. Post-hooks don't affect the Git operation since they run after completion.
Q: How do I debug Git hooks?
Add debug output using echo
statements, check the hook's exit status with echo $?
, and test hooks manually by running them directly from the command line.
Conclusion
Git hooks are powerful tools that automate code quality checks and enforce development standards. By implementing the hooks and practices outlined in this guide, you'll create a more robust development workflow that catches issues early and maintains consistent code quality.
Key takeaways:
- Start simple with basic linting and formatting hooks
- Use hook managers to simplify setup and maintenance
- Keep hooks fast to avoid disrupting developer flow
- Provide clear error messages and recovery options
- Share hooks across your team for consistent standards
Begin implementing Git hooks in your projects today. Start with a simple pre-commit hook for linting, then gradually add more sophisticated checks as your team becomes comfortable with the workflow.
Have you implemented Git hooks in your projects? Share your experiences and custom hook scripts in the comments below!
Add Comment
No comments yet. Be the first to comment!