Git hooks are scripts that run automatically at specific points in the Git workflow. They allow you to customize Git’s behavior, enforce policies, automate tasks, and integrate with external tools.
What Are Git Hooks?
Hooks are programs placed in the .git/hooks/ directory (or configured via core.hooksPath). When certain Git actions occur, Git looks for corresponding hook scripts and executes them.
Hook directory structure:
.git/hooks/
├── pre-commit.sample
├── pre-push.sample
├── commit-msg.sample
└── ...
To activate a hook, remove the .sample extension and make it executable:
cd .git/hooks
mv pre-commit.sample pre-commit
chmod +x pre-commit
Hook Categories
Client-Side Hooks
Triggered by local operations like committing and merging.
Server-Side Hooks
Triggered by network operations like receiving pushed commits.
Common Client-Side Hooks
pre-commit
When: Before creating a commit
Use cases:
- Run linters
- Check code style
- Run tests
- Prevent commits with TODO comments
Example: Run tests before commit
#!/bin/bash
# .git/hooks/pre-commit
echo "Running tests..."
if ! npm test; then
echo "Tests failed. Commit aborted."
exit 1
fi
Bypass: git commit --no-verify
prepare-commit-msg
When: After preparing default commit message, before editor opens
Use cases:
- Add issue number from branch name
- Insert commit template
- Add metadata
Example: Add branch name to commit message
#!/bin/bash
# .git/hooks/prepare-commit-msg
COMMIT_MSG_FILE=$1
COMMIT_SOURCE=$2
# Only for regular commits (not merge, squash, etc.)
if [ -z "$COMMIT_SOURCE" ]; then
BRANCH=$(git symbolic-ref --short HEAD)
# If branch is feature/JIRA-123-description, add JIRA-123 to message
ISSUE=$(echo "$BRANCH" | grep -o '[A-Z]\+-[0-9]\+')
if [ -n "$ISSUE" ]; then
echo "[$ISSUE] $(cat "$COMMIT_MSG_FILE")" > "$COMMIT_MSG_FILE"
fi
fi
commit-msg
When: After commit message is entered
Use cases:
- Enforce commit message format
- Check for issue references
- Validate message length
Example: Enforce conventional commits
#!/bin/bash
# .git/hooks/commit-msg
COMMIT_MSG_FILE=$1
COMMIT_MSG=$(cat "$COMMIT_MSG_FILE")
# Check for conventional commit format: type(scope): subject
if ! echo "$COMMIT_MSG" | grep -qE "^(feat|fix|docs|style|refactor|test|chore)(\(.+\))?: .{1,}"; then
echo "Error: Commit message must follow Conventional Commits format"
echo "Examples:"
echo " feat(auth): add OAuth2 support"
echo " fix(api): handle null responses"
echo " docs: update README"
exit 1
fi
post-commit
When: After a commit is created
Use cases:
- Notifications
- Trigger CI/CD
- Update documentation
Example: Notify of commit
#!/bin/bash
# .git/hooks/post-commit
COMMIT=$(git log -1 --pretty=format:"%h - %s")
notify-send "Git Commit" "$COMMIT"
pre-push
When: Before pushing to remote
Use cases:
- Run full test suite
- Check for sensitive data
- Prevent force pushes to protected branches
Example: Prevent push to main without tests
#!/bin/bash
# .git/hooks/pre-push
REMOTE=$1
URL=$2
while read local_ref local_sha remote_ref remote_sha; do
if [ "$remote_ref" = "refs/heads/main" ]; then
echo "Running full test suite before pushing to main..."
if ! npm run test:all; then
echo "Tests failed. Push aborted."
exit 1
fi
fi
done
Bypass: git push --no-verify
pre-rebase
When: Before rebasing
Use cases:
- Prevent rebasing published commits
- Check for uncommitted changes
Example: Prevent rebasing published branches
#!/bin/bash
# .git/hooks/pre-rebase
UPSTREAM=$1
BRANCH=$2
# Prevent rebasing main or develop
if [ "$BRANCH" = "main" ] || [ "$BRANCH" = "develop" ]; then
echo "Error: Never rebase $BRANCH"
exit 1
fi
post-checkout
When: After checking out a branch
Use cases:
- Update dependencies
- Clear build artifacts
- Set up environment
Example: Install dependencies on branch switch
#!/bin/bash
# .git/hooks/post-checkout
PREV_HEAD=$1
NEW_HEAD=$2
BRANCH_SWITCH=$3
if [ $BRANCH_SWITCH -eq 1 ]; then
# Check if package.json changed
if git diff --name-only $PREV_HEAD $NEW_HEAD | grep -q package.json; then
echo "package.json changed. Running npm install..."
npm install
fi
fi
post-merge
When: After a merge
Use cases:
- Update dependencies
- Rebuild project
- Database migrations
Example: Auto-update dependencies after merge
#!/bin/bash
# .git/hooks/post-merge
if git diff-tree -r --name-only --no-commit-id ORIG_HEAD HEAD | grep -q "^package.json"; then
echo "package.json changed. Running npm install..."
npm install
fi
Server-Side Hooks
pre-receive
When: Before accepting any pushed commits
Use cases:
- Enforce policies across all refs
- Check commit messages
- Prevent force pushes
Example: Prevent force push to main
#!/bin/bash
# .git/hooks/pre-receive
while read old_sha new_sha ref_name; do
if [ "$ref_name" = "refs/heads/main" ]; then
# Check if this is a force push
if [ "$old_sha" != "0000000000000000000000000000000000000000" ]; then
if ! git merge-base --is-ancestor $old_sha $new_sha; then
echo "Error: Force push to main is not allowed"
exit 1
fi
fi
fi
done
update
When: Before updating each individual ref
Use cases:
- Branch-specific access control
- Enforce naming conventions
- Prevent deletion of protected branches
Example: Enforce branch naming
#!/bin/bash
# .git/hooks/update
REF_NAME=$1
OLD_SHA=$2
NEW_SHA=$3
# Extract branch name
BRANCH=$(echo $REF_NAME | sed 's/refs\/heads\///')
# Enforce branch naming: feature/*, bugfix/*, hotfix/*
if ! echo $BRANCH | grep -qE '^(feature|bugfix|hotfix|main|develop)/'; then
if [ "$BRANCH" != "main" ] && [ "$BRANCH" != "develop" ]; then
echo "Error: Branch name must start with feature/, bugfix/, or hotfix/"
exit 1
fi
fi
post-receive
When: After all refs are updated
Use cases:
- Trigger CI/CD
- Send notifications
- Deploy code
- Update issue trackers
Example: Trigger deployment
#!/bin/bash
# .git/hooks/post-receive
while read old_sha new_sha ref_name; do
if [ "$ref_name" = "refs/heads/main" ]; then
echo "Main branch updated. Triggering deployment..."
curl -X POST https://ci.example.com/deploy \
-H "Content-Type: application/json" \
-d '{"branch": "main", "commit": "'$new_sha'"}'
fi
done
Advanced Hook Techniques
Sharing Hooks with Your Team
Create a hooks directory in your repository
cp .git/hooks/pre-commit git-hooks/
git config core.hooksPath git-hooks
git add git-hooks/
git commit -m "Add shared Git hooks"
Team members can now use the same hooks
git config core.hooksPath git-hooks
Hooks in Multiple Languages
Hooks can be written in any language:
Python:
#!/usr/bin/env python3
# .git/hooks/pre-commit
import sys
import subprocess
def run_tests():
result = subprocess.run(['pytest'], capture_output=True)
return result.returncode == 0
if __name__ == '__main__':
if not run_tests():
print("Tests failed. Commit aborted.")
sys.exit(1)
Node.js:
#!/usr/bin/env node
// .git/hooks/pre-commit
const { execSync } = require('child_process');
try {
execSync('npm run lint', { stdio: 'inherit' });
} catch (error) {
console.error('Linting failed. Commit aborted.');
process.exit(1);
}
Several tools make hook management easier:
Husky (Node.js):
npm install --save-dev husky
npx husky init
package.json:
{
"scripts": {
"prepare": "husky"
}
}
pre-commit (Python):
# .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
pip install pre-commit
pre-commit install
Lefthook (Go):
# lefthook.yml
pre-commit:
commands:
lint:
run: npm run lint
tests:
run: npm test
Best Practices
-
Keep hooks fast - Slow hooks disrupt workflow. Run intensive operations asynchronously
-
Make hooks skippable - Always allow
--no-verify for emergencies
-
Provide clear error messages - Tell users what went wrong and how to fix it
-
Version control hooks - Use
core.hooksPath to share hooks with the team
-
Use hook managers - Tools like Husky make hook setup easier
-
Test your hooks - Hooks are code; treat them as such
-
Document requirements - Explain what tools hooks need (linters, test runners, etc.)
-
Fail gracefully - Handle missing dependencies and provide helpful messages
Security Considerations
Hooks are code - They execute with your permissions. Never blindly copy hooks from untrusted sources.Review before enabling - When cloning a repository, review hook scripts before configuring core.hooksPath.Server-side validation - Never rely solely on client-side hooks for security. Enforce policies on the server.
Troubleshooting
Hook Not Executing
Check permissions:
ls -la .git/hooks/
chmod +x .git/hooks/pre-commit
Verify hook path:
git config core.hooksPath
Check for errors:
Add debugging to your hook:
#!/bin/bash
set -x # Print commands as they execute
# Your hook code
Hook Failures
View hook output:
Hooks write to stderr/stdout. Review the output carefully.
Bypass temporarily:
Test hook manually:
# Run the hook script directly
.git/hooks/pre-commit
echo $? # Check exit code
Complete Example: Pre-Commit Quality Checks
#!/bin/bash
# .git/hooks/pre-commit
set -e # Exit on first error
echo "Running pre-commit checks..."
# Check for trailing whitespace
echo "Checking for trailing whitespace..."
if git diff-index --check --cached HEAD --; then
echo "✓ No trailing whitespace"
else
echo "✗ Fix trailing whitespace before committing"
exit 1
fi
# Run linter
echo "Running linter..."
if npm run lint; then
echo "✓ Linting passed"
else
echo "✗ Linting failed"
exit 1
fi
# Run unit tests
echo "Running unit tests..."
if npm run test:unit; then
echo "✓ Unit tests passed"
else
echo "✗ Unit tests failed"
exit 1
fi
# Check for debugging statements
echo "Checking for debug statements..."
if git diff --cached | grep -E "(console\.log|debugger|binding\.pry)"; then
echo "✗ Remove debugging statements before committing"
exit 1
fi
echo "✓ All pre-commit checks passed!"
exit 0
Hook Reference
All Available Hooks
Commit workflow:
pre-commit
prepare-commit-msg
commit-msg
post-commit
Email workflow:
applypatch-msg
pre-applypatch
post-applypatch
Other client hooks:
pre-rebase
post-checkout
post-merge
pre-push
pre-auto-gc
post-rewrite
Server hooks:
pre-receive
update
post-receive
post-update
push-to-checkout
reference-transaction
Configuration
Useful hook-related configurations:
# Set custom hooks directory
git config core.hooksPath /path/to/hooks
# Disable specific hooks for a repository
git config hooks.pre-commit false