Concepts

Git Hooks Deep Dive

Understand Git Hooks types, lifecycle, sharing mechanisms, and CI/CD integration practices.

Who This Is For
  • Readers who want the history model before advanced commands
Prerequisites
  • A basic sense that commits are not just a file list
Common Risks
  • Treating a concepts page like a command how-to

Overview

Git Hooks are custom scripts triggered at key points during Git operations. They're not built-in Git features but leverage Git's event notification mechanism to run external programs.

Hook Lifecycle

flowchart LR
  subgraph Local
    A[pre-commit] --> B[prepare-commit-msg]
    B --> C[commit-msg]
    C --> D[post-commit]
  end
  subgraph Push
    D --> E[pre-push]
  end
  subgraph Remote
    E --> F[pre-receive]
    F --> G[update]
    G --> H[post-receive]
  end

Core Hooks

Client-Side Hooks

HookTriggerReturn Value Effect
pre-commitBefore git commitNon-zero aborts commit
prepare-commit-msgBefore editor opensCan modify default message
commit-msgAfter message is savedNon-zero aborts commit
post-commitAfter commit completesReturn ignored
pre-pushBefore git pushNon-zero aborts push
pre-rebaseBefore git rebaseNon-zero aborts rebase
post-checkoutAfter git checkoutReturn ignored
post-mergeAfter git mergeReturn ignored

Server-Side Hooks

HookTriggerDescription
pre-receiveBefore receiving ref updatesCan reject pushes
updateBefore each ref updatePer-branch control
post-receiveAfter ref updates completeTrigger CI, notifications

Practical Hook Examples

pre-commit: Code Quality

#!/bin/bash
# .git/hooks/pre-commit
echo "Running linter..."
npm run lint
if [ $? -ne 0 ]; then
  echo "Lint failed. Commit aborted."
  exit 1
fi

commit-msg: Message Convention

#!/bin/bash
# .git/hooks/commit-msg
PATTERN="^(feat|fix|docs|style|refactor|test|chore|ci)(\(.+\))?: .{1,72}"
msg=$(cat "$1")
if ! [[ "$msg" =~ $PATTERN ]]; then
  echo "Error: commit message must match Conventional Commits format"
  echo "  e.g. feat(api): add login endpoint"
  exit 1
fi

pre-push: Protected Branch Check

#!/bin/bash
# .git/hooks/pre-push
protected_branches="main master develop"
while read local_ref local_sha remote_ref remote_sha; do
  for branch in $protected_branches; do
    if [[ "$remote_ref" == "refs/heads/$branch" ]]; then
      echo "Error: pushing to $branch is not allowed via hook"
      exit 1
    fi
  done
done
exit 0

Sharing Hooks

Method 1: In-Repository (Recommended)

# Project structure
my-repo/
├── .githooks/
│   ├── pre-commit
│   └── commit-msg
└── .gitignore

# Configure custom hooks directory
git config core.hooksPath .githooks

Method 2: Global Hooks

# Create global hooks directory
mkdir -p ~/.git-hooks
git config --global core.hooksPath ~/.git-hooks

Method 3: husky (Node.js Projects)

npx husky init

# .husky/pre-commit
npx lint-staged

CI Integration

# GitHub Actions
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - name: Check commit messages
        run: |
          git log --format=%s -1 | grep -E '^(feat|fix|docs)'

Security Notes

  1. Server-side hooks first: Client hooks can be bypassed (git commit --no-verify)
  2. Don't rely on client hooks for security: Server-side pre-receive is the real defense
  3. Hook script permissions: Ensure hooks have execute permission (chmod +x)
  4. Performance impact: Complex hooks can significantly slow Git operations

Continue Learning

  1. concepts/git-hooks — Git Hooks basics
  2. workflows/pre-commit-hook-workflow — Pre-commit hook workflow
  3. concepts/git-lfs-deep — Git LFS deep dive