Skip to main content
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

1
Create a hooks directory in your repository
2
mkdir git-hooks
3
Add your hooks
4
cp .git/hooks/pre-commit git-hooks/
5
Configure Git to use this directory
6
git config core.hooksPath git-hooks
7
Commit the hooks
8
git add git-hooks/
git commit -m "Add shared Git hooks"
9
Team members can now use the same hooks
10
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);
}

Hook Tools and Frameworks

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
lefthook install

Best Practices

  1. Keep hooks fast - Slow hooks disrupt workflow. Run intensive operations asynchronously
  2. Make hooks skippable - Always allow --no-verify for emergencies
  3. Provide clear error messages - Tell users what went wrong and how to fix it
  4. Version control hooks - Use core.hooksPath to share hooks with the team
  5. Use hook managers - Tools like Husky make hook setup easier
  6. Test your hooks - Hooks are code; treat them as such
  7. Document requirements - Explain what tools hooks need (linters, test runners, etc.)
  8. 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:
git commit --no-verify
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