Git Hooks and Automation¶
Git hooks are scripts that run automatically at specific points in the Git workflow - before a commit, after a merge, before a push. They let you enforce coding standards, run tests, validate commit messages, and automate repetitive tasks. This guide covers every hook, how to write them, hook management frameworks, and Git's built-in tools for debugging and forensic investigation.
How Hooks Work¶
Hooks are executable scripts stored in .git/hooks/. When Git reaches a trigger point (like committing), it checks for a hook with the corresponding name and runs it if found.
- Hooks must be executable (
chmod +x .git/hooks/pre-commit) - They can be written in any language (bash, Python, Ruby, Node.js - as long as the shebang line is correct)
- Client-side hooks run on your machine. They're not pushed or shared through the repository (
.git/hooks/is local). - Server-side hooks run on the remote when receiving pushes.
- Hooks that exit with non-zero status abort the operation they guard.
Hooks are local
Client-side hooks live in .git/hooks/, which isn't tracked by Git. You can't enforce them through the repository alone. That's why hook frameworks (covered below) exist - they let you commit hook definitions that teammates install locally.
Client-Side Hooks¶
Pre-Commit Hooks¶
| Hook | Runs | Purpose |
|---|---|---|
pre-commit |
Before commit message editor opens | Validate the code being committed |
prepare-commit-msg |
After default message is created, before editor opens | Modify the commit message template |
commit-msg |
After you write the message, before commit is created | Validate the commit message format |
post-commit |
After commit is created | Notifications, logging |
pre-commit¶
The most commonly used hook. It runs before the commit message editor opens. If it exits non-zero, the commit is aborted. Use it for linting, formatting checks, and preventing debug code from being committed.
#!/bin/bash
# .git/hooks/pre-commit - Check for debug statements
# Check staged Python files for debug prints
FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.py$')
if [ -n "$FILES" ]; then
if grep -n 'import pdb\|breakpoint()\|print(' $FILES; then
echo "ERROR: Debug statements found in staged files."
echo "Remove them before committing."
exit 1
fi
fi
exit 0
commit-msg¶
Receives the commit message file path as its argument. Use it to enforce message conventions:
#!/bin/bash
# .git/hooks/commit-msg - Enforce Conventional Commits format
MSG_FILE=$1
MSG=$(cat "$MSG_FILE")
# Check for conventional commit prefix
if ! echo "$MSG" | grep -qE '^(feat|fix|docs|refactor|test|chore|style|perf|ci|build|revert)(\(.+\))?: .+'; then
echo "ERROR: Commit message must follow Conventional Commits format:"
echo " feat: add new feature"
echo " fix(auth): resolve login timeout"
echo " docs: update API guide"
echo ""
echo "Your message: $MSG"
exit 1
fi
exit 0
Other Client-Side Hooks¶
| Hook | Runs | Purpose |
|---|---|---|
pre-rebase |
Before rebase starts | Prevent rebasing certain branches |
pre-push |
Before push transmits data | Run tests before pushing |
post-checkout |
After git checkout/git switch |
Set up environment, update dependencies |
post-merge |
After a successful merge | Install dependencies, rebuild |
pre-auto-gc |
Before automatic garbage collection | Notify or prevent GC |
The pre-push hook is particularly useful for running a quick test suite before pushing:
#!/bin/bash
# .git/hooks/pre-push - Run tests before push
echo "Running tests before push..."
npm test
if [ $? -ne 0 ]; then
echo "Tests failed. Push aborted."
exit 1
fi
exit 0
Server-Side Hooks¶
Server-side hooks run on the remote repository when receiving pushes. They're managed by the server administrator, not individual developers.
| Hook | Runs | Purpose |
|---|---|---|
pre-receive |
Before any refs are updated | Global policy enforcement |
update |
Once per branch being updated | Per-branch policy enforcement |
post-receive |
After all refs are updated | Notifications, CI triggers, deploys |
pre-receive is the enforcement point for server-side rules. If it exits non-zero, the entire push is rejected.
Hook Frameworks¶
Since hooks aren't committed to the repository, teams need a way to share and enforce them. Hook frameworks solve this.
pre-commit (Python)¶
pre-commit is a framework that manages hook installation from a shared config file:
# Install
pip install pre-commit
# Create .pre-commit-config.yaml in your repo
# Install hooks based on the config
pre-commit install
# .pre-commit-config.yaml
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
args: ['--maxkb=500']
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.3.0
hooks:
- id: ruff
args: ['--fix']
- id: ruff-format
Husky (Node.js)¶
Husky integrates with npm/yarn projects:
npx husky init
echo "npm test" > .husky/pre-commit
echo "npx commitlint --edit \$1" > .husky/commit-msg
Lefthook¶
Lefthook is a fast, polyglot hook manager:
# lefthook.yml
pre-commit:
parallel: true
commands:
lint:
glob: "*.{js,ts}"
run: npx eslint {staged_files}
format:
glob: "*.py"
run: ruff format --check {staged_files}
Git Bisect: Binary Search for Bugs¶
git bisect performs a binary search through commit history to find which commit introduced a bug. Instead of checking every commit, it cuts the search space in half each step.
Manual Bisect¶
# Start bisecting
git bisect start
# Mark the current commit as bad (has the bug)
git bisect bad
# Mark an older commit as good (doesn't have the bug)
git bisect good v1.0
# Git checks out a commit halfway between good and bad
# Test it, then mark:
git bisect good # if this commit doesn't have the bug
git bisect bad # if this commit has the bug
# Git narrows the range and checks out the next midpoint
# Repeat until the first bad commit is found
# When done
git bisect reset
Automated Bisect¶
If you have a script that returns 0 for good and non-zero for bad:
git bisect start HEAD v1.0
git bisect run npm test
# or
git bisect run python -m pytest tests/test_auth.py
Git runs the script at each step automatically and reports the first bad commit.
Git Blame and Forensic Investigation¶
git blame¶
git blame annotates each line of a file with the commit that last modified it:
# Blame a file
git blame src/auth.py
# Ignore whitespace changes
git blame -w src/auth.py
# Show original author even after move/copy
git blame -C src/auth.py
# Blame a specific range of lines
git blame -L 10,20 src/auth.py
# Show blame at a specific commit (before a refactor)
git blame v1.0 -- src/auth.py
Code Search with git log¶
# Find commits where a string was added/removed (pickaxe)
git log -S "authenticate" --oneline
# Find commits where a regex was added/removed in the diff
git log -G "def authenticate\(" --oneline
# Search with patch output to see the actual changes
git log -S "authenticate" -p
# Combine with file path
git log -S "authenticate" -- src/auth.py
Exercises¶
Further Reading¶
- Pro Git - Chapter 8.3: Git Hooks - comprehensive hook documentation
- Official githooks documentation - complete reference for all hook types
- pre-commit.com - Python-based hook framework
- Husky Documentation - Node.js hook management
- Lefthook Documentation - polyglot hook manager
- Official git-bisect documentation - binary search debugging
- Official git-blame documentation - line-level authorship annotation
Previous: GitHub, GitLab, and Bitbucket | Next: Git Security | Back to Index