git-hooks

SKILL.md

Git Hooks

Automate code quality, testing, and validation using Git hooks—scripts that run automatically at key points in the Git workflow.

What Are Git Hooks

Git hooks are executable scripts that Git runs automatically when specific events occur in a repository. They enable you to:

  • Enforce code quality before commits reach the repository
  • Run automated tests to catch issues early
  • Validate commit messages to maintain consistent standards
  • Prevent accidental destructive actions like force pushes
  • Trigger CI/CD workflows on push events
  • Automate versioning and tagging for releases

Hooks reside in .git/hooks/ by default, but can be version-controlled using core.hooksPath configuration.

Core Concepts

Hook Types

Client-side hooks (run on developer machines):

  • pre-commit - Before commit is created, validate staged changes
  • prepare-commit-msg - Populate commit message template
  • commit-msg - Validate commit message format
  • post-commit - Notification after successful commit
  • pre-push - Before pushing to remote, run tests
  • post-checkout - After checkout, clean up working directory
  • pre-rebase - Before rebasing, check for conflicts

Server-side hooks (run on remote repository):

  • pre-receive - Before accepting pushed refs, enforce policies
  • update - Like pre-receive, but runs per branch
  • post-receive - After successful push, trigger CI/CD

Hook Lifecycle

Developer action → Git event → Hook script runs → Exit code determines outcome
  • Exit 0: Continue with Git operation
  • Exit non-zero: Abort Git operation with error message

Making Hooks Executable

Hooks must have execute permissions:

chmod +x .git/hooks/pre-commit

Essential Workflows

Setting Up Version-Controlled Hooks

Git doesn't version-control .git/hooks/ by default. Use core.hooksPath to enable team-wide hooks:

1. Create hooks directory in repository:

mkdir .githooks

2. Configure Git to use custom hooks path:

git config core.hooksPath .githooks

3. Add hooks to version control:

git add .githooks/
git commit -m "Add version-controlled git hooks"

4. Team members run after cloning:

git config core.hooksPath .githooks

This project follows this pattern. See .githooks/ directory for working examples.

Creating a Basic Pre-Commit Hook

Use case: Validate bash scripts before committing.

1. Create hook file:

touch .githooks/pre-commit
chmod +x .githooks/pre-commit

2. Add validation logic:

#!/bin/bash
set -e

echo "🔍 Validating bash scripts..."

# Get staged .sh files
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.sh$' || true)

if [ -z "$STAGED_FILES" ]; then
    echo "✅ No bash scripts to validate"
    exit 0
fi

# Validate syntax
for file in $STAGED_FILES; do
    if [ -f "$file" ]; then
        echo "Checking $file..."
        bash -n "$file" || exit 1
    fi
done

echo "✅ All bash scripts valid"
exit 0

Key patterns:

  • Use set -e to fail fast on errors
  • Check for staged files with git diff --cached
  • Exit early if no relevant files
  • Provide clear visual feedback with emojis
  • Exit with non-zero on validation failure

Modular Hook Architecture

Problem: Single hook file becomes complex with multiple validations.

Solution: Orchestrator pattern that discovers and runs modular hook scripts.

1. Create hooks.d/ directory:

mkdir .githooks/hooks.d

2. Create orchestrator in pre-commit:

#!/bin/bash
set -e

HOOKS_DIR="$(dirname "$0")/hooks.d"

if [ -d "$HOOKS_DIR" ]; then
    for hook in "$HOOKS_DIR"/*; do
        if [ -x "$hook" ]; then
            echo "Running: $(basename "$hook")"
            "$hook" || exit 1
        fi
    done
fi

exit 0

3. Add individual validation scripts:

# .githooks/hooks.d/10-validate-bash.sh
# .githooks/hooks.d/20-validate-yaml.sh
# .githooks/hooks.d/30-run-tests.sh

Naming convention: Use numbered prefixes (10, 20, 30) to control execution order and allow inserting new hooks between existing ones (e.g., add a hypothetical 15-validate-json.sh between 10 and 20).

This project uses this pattern. See .githooks/hooks.d/ for examples.

Validating Commit Messages

Use case: Enforce conventional commit format.

Create commit-msg hook:

#!/bin/bash

COMMIT_MSG_FILE=$1
COMMIT_MSG=$(cat "$COMMIT_MSG_FILE")

# Pattern: type(scope): description
PATTERN="^(feat|fix|docs|style|refactor|test|chore)(\(.+\))?: .{10,}$"

if ! echo "$COMMIT_MSG" | grep -qE "$PATTERN"; then
    echo "❌ Invalid commit message format"
    echo ""
    echo "Expected format: type(scope): description"
    echo "Types: feat, fix, docs, style, refactor, test, chore"
    echo "Example: feat(auth): add OAuth2 login support"
    exit 1
fi

echo "✅ Commit message valid"
exit 0

Running Tests Before Push

Use case: Prevent pushing broken code to remote.

Create pre-push hook:

#!/bin/bash
set -e

echo "🧪 Running tests before push..."

# Run test suite
npm test || {
    echo "❌ Tests failed. Push aborted."
    exit 1
}

echo "✅ All tests passed"
exit 0

Preventing Force Push to Main

Use case: Protect production branches from destructive operations.

Create pre-push hook:

#!/bin/bash

PROTECTED_BRANCHES="^(main|master|production)$"

while read local_ref local_sha remote_ref remote_sha; do
    remote_branch=$(echo "$remote_ref" | sed 's/refs\/heads\///')
    
    if echo "$remote_branch" | grep -qE "$PROTECTED_BRANCHES"; then
        # Check if it's a force push
        if [ "$local_sha" = "0000000000000000000000000000000000000000" ]; then
            echo "❌ Deleting $remote_branch is not allowed"
            exit 1
        fi
        
        # Check for force push
        if ! git merge-base --is-ancestor "$remote_sha" "$local_sha" 2>/dev/null; then
            echo "❌ Force push to $remote_branch is not allowed"
            exit 1
        fi
    fi
done

exit 0

Project-Specific Implementation

This repository implements a production-ready Git hooks system. Follow this implementation pattern for your projects.

Directory Structure

.githooks/
├── pre-commit                    # Orchestrator script
├── hooks.d/                      # Individual validation scripts
│   ├── the-perfect-developer-base-collection-10-validate-bash.sh
│   └── the-perfect-developer-base-collection-20-validate-skills.sh
└── README.md                     # Documentation

Installation

Automated setup:

./setup-hooks.sh

This configures git config core.hooksPath .githooks automatically.

Hook Naming Convention

Use this pattern for modular hooks:

the-perfect-developer-base-collection-<number>-<description>.sh
  • Prefix: Project/team identifier
  • Number: Execution order (increments of 10: 10, 20, 30...)
  • Description: What the hook validates

Increments of 10 allow inserting new hooks between existing ones (e.g., add 15-validate-json.sh between 10 and 20).

Testing Individual Hooks

Run hooks independently for testing:

# Test single hook
.githooks/hooks.d/10-validate-bash.sh

# Test orchestrator
.githooks/pre-commit

# Temporarily disable a hook
chmod -x .githooks/hooks.d/20-validate-skills.sh
# Or rename with .disabled extension
mv .githooks/hooks.d/20-validate-skills.sh{,.disabled}

Hook Template

Use this template for new validation hooks:

#!/bin/bash
set -e

echo "🔍 Running [VALIDATION NAME]..."

# Get staged files matching pattern
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep 'pattern' || true)

if [ -z "$STAGED_FILES" ]; then
    echo "✅ No files to validate"
    exit 0
fi

# Validation logic
for file in $STAGED_FILES; do
    if [ -f "$file" ]; then
        echo "Validating $file..."
        # Add validation command here
        # validation-command "$file" || exit 1
    fi
done

echo "✅ Validation passed"
exit 0

Hook Parameters and Environment

Common Parameters

Different hooks receive different parameters:

pre-commit: No parameters

#!/bin/bash
# Access staged files via git commands
STAGED=$(git diff --cached --name-only)

commit-msg: Commit message file path

#!/bin/bash
COMMIT_MSG_FILE=$1
message=$(cat "$COMMIT_MSG_FILE")

pre-push: stdin with refs being pushed

#!/bin/bash
while read local_ref local_sha remote_ref remote_sha; do
    # Process each ref
done

post-checkout: prev HEAD, new HEAD, branch flag

#!/bin/bash
PREV_HEAD=$1
NEW_HEAD=$2
IS_BRANCH=$3  # 1 for branch checkout, 0 for file checkout

Environment Variables

Git sets environment variables hooks can access:

  • GIT_DIR - Path to .git directory
  • GIT_WORK_TREE - Path to working directory
  • GIT_INDEX_FILE - Path to index file
  • GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL - Commit author
  • GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL - Committer info

Test environment variables:

#!/bin/bash
echo "Running $BASH_SOURCE"
set | grep GIT
echo "PWD is $PWD"

Best Practices

DO:

  • Make hooks fast—developers run them frequently
  • Exit early when no relevant files are staged
  • Provide clear, actionable error messages
  • Use visual indicators (✅ ❌ 🔍) for quick scanning
  • Version-control hooks using core.hooksPath
  • Test hooks independently before integration
  • Document hook behavior in README
  • Use modular architecture for complex validation
  • Make hooks easy to temporarily disable

DON'T:

  • Perform long-running operations in pre-commit
  • Block commits without clear explanation
  • Hardcode file paths—use relative paths
  • Assume dependencies are installed—check first
  • Create infinite loops or complex recursion
  • Mix multiple concerns in one hook script
  • Skip error handling and validation
  • Force hooks on team without consensus

Security Considerations

Validate inputs: Never trust user input in hooks

# Bad: Command injection risk
git diff --cached --name-only | xargs some-command

# Good: Proper quoting and validation
git diff --cached --name-only | while read file; do
    if [ -f "$file" ]; then
        some-command "$file"
    fi
done

Check permissions: Ensure hooks are executable by intended users only

chmod 755 .githooks/pre-commit  # Owner write, others read+execute

Avoid secrets: Never hardcode credentials in hooks

# Bad: Hardcoded token
API_TOKEN="secret123"

# Good: Read from environment
API_TOKEN="${API_TOKEN:-$(cat ~/.api_token)}"

Review third-party hooks: Understand what scripts do before using them.

Troubleshooting

Hook not running:

  1. Check execute permissions: ls -l .githooks/pre-commit
  2. Verify core.hooksPath config: git config core.hooksPath
  3. Ensure file has no .sample extension
  4. Check shebang line is correct: #!/bin/bash

Hook runs but fails unexpectedly:

  1. Run hook manually to see errors: .githooks/pre-commit
  2. Check exit codes: echo $? after running
  3. Verify dependencies are installed
  4. Test with minimal staged changes
  5. Add debug output: set -x at start of script

Cannot commit:

  1. Read hook error message carefully
  2. Fix validation issues or unstage problematic files
  3. Temporarily disable hook if needed: git commit --no-verify
  4. Note: --no-verify bypasses all hooks—use sparingly

Hooks not version-controlled:

  1. Ensure using core.hooksPath, not .git/hooks/
  2. Verify .githooks/ is committed: git ls-files .githooks/
  3. Check team members ran setup: git config core.hooksPath

Additional Resources

Reference Documentation

For detailed information on advanced topics:

  • references/hook-types.md - Complete list of all Git hooks with parameters and use cases
  • references/server-side-hooks.md - Server-side hooks for CI/CD and policy enforcement
  • references/advanced-patterns.md - Complex validation patterns and techniques
  • references/ci-cd-integration.md - Integrating hooks with continuous integration pipelines

Example Implementations

Working examples from this project:

  • examples/modular-pre-commit/ - Orchestrator pattern with hooks.d/ directory
  • examples/validate-bash.sh - Bash syntax validation hook
  • examples/validate-skills.sh - SKILL.md validation hook
  • examples/setup-hooks.sh - Installation script for team setup

External Resources

Official Git documentation:

Comprehensive tutorials:

Quick Reference

Configure version-controlled hooks:

git config core.hooksPath .githooks

Make hook executable:

chmod +x .githooks/pre-commit

Test hook manually:

.githooks/pre-commit

Bypass hooks temporarily:

git commit --no-verify

Common hook skeleton:

#!/bin/bash
set -e
echo "🔍 Running validation..."
# Validation logic here
echo "✅ Validation passed"
exit 0

Get staged files:

git diff --cached --name-only --diff-filter=ACM

Validate and exit on failure:

command || exit 1
Weekly Installs
3
GitHub Stars
1
First Seen
1 day ago
Installed on
opencode3
cline2
github-copilot2
codex2
kimi-cli2
gemini-cli2