Skip to content

Git Hooks Tutorial โ€” Automate Your Git Workflow

DodaTech 4 min read

In this tutorial, you'll learn about Git Hooks Tutorial. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

What You'll Learn

Use Git hooks to automate tasks โ€” lint code before commits, enforce commit message formats, run tests before push, and trigger deployments.

Why It Matters

Git hooks prevent bad commits before they enter your history. They enforce your team's standards automatically โ€” no more "forgot to lint" or "wrong commit format."

Real-World Use

Blocking a commit if tests fail, ensuring every commit follows Conventional Commits, or running a linter before allowing code to be pushed.

What are Git Hooks?

Git hooks are scripts that run automatically at specific points in Git's workflow. They're stored in .git/hooks/ in every Repository.

Available Hooks

Client-Side Hooks

Hook When It Runs Common Use
pre-commit Before commit message Lint, format, check for secrets
prepare-commit-msg Before commit message editor Auto-populate message
commit-msg After commit message Validate message format
post-commit After commit Notifications
pre-push Before push Run tests, check branches
post-checkout After checkout Update dependencies
post-merge After merge Install new dependencies
pre-rebase Before rebase Prevent rebase on certain branches

Server-Side Hooks

Hook When It Runs Common Use
pre-receive Before push is accepted Enforce policies
update Per branch, during push Branch-specific rules
post-receive After push accepted Deploy, CI triggers

Pre-Commit Hook โ€” Lint Before Commit

#!/bin/bash
# .git/hooks/pre-commit

echo "Running linter..."

# Check for lint errors
npm run lint
if [ $? -ne 0 ]; then
    echo "โŒ Linting failed. Fix errors before committing."
    exit 1
fi

# Check for debugger statements
if grep -r "debugger" --include="*.js" --include="*.ts" .; then
    echo "โŒ Found debugger statements. Remove them."
    exit 1
fi

# Check for API keys
if grep -r "sk-[A-Za-z0-9_-]\{20,\}" --include="*.js" .; then
    echo "โŒ Possible API key detected. Remove it."
    exit 1
fi

echo "โœ… All checks passed"
exit 0

Commit-Msg Hook โ€” Enforce Conventional Commits

#!/bin/bash
# .git/hooks/commit-msg

commit_msg=$(cat "$1")

# Format: type(scope): description
pattern="^(feat|fix|docs|style|refactor|test|chore|perf|ci|build)\([a-z-]+\): .{1,}$"

if ! [[ "$commit_msg" =~ $pattern ]]; then
    echo "โŒ Invalid commit message format."
    echo ""
    echo "Expected format: type(scope): description"
    echo ""
    echo "Types: feat, fix, docs, style, refactor, test, chore, perf, ci, build"
    echo ""
    echo "Example: feat(login): add OAuth authentication"
    echo ""
    echo "Your message: $commit_msg"
    exit 1
fi

exit 0

Pre-Push Hook โ€” Run Tests

#!/bin/bash
# .git/hooks/pre-push

echo "๐Ÿงช Running tests before push..."

# Run the test suite
npm test
if [ $? -ne 0 ]; then
    echo "โŒ Tests failed. Push rejected."
    exit 1
fi

# Check if pushing to main without PR
while read local_ref local_sha remote_ref remote_sha; do
    if [[ "$remote_ref" == "refs/heads/main" ]]; then
        echo "โŒ Direct push to main is not allowed. Use a PR."
        exit 1
    fi
done

echo "โœ… All checks passed. Pushing..."
exit 0

Using Husky (Node.js)

Husky makes hooks manageable and shareable:

npm install --save-dev husky

# package.json
{
  "scripts": {
    "prepare": "husky install"
  }
}
npx husky add .husky/pre-commit "npx lint-staged"
npx husky add .husky/commit-msg "npx commitlint --edit $1"
npx husky add .husky/pre-push "npm test"

Pre-Commit Hooks with Python

#!/usr/bin/env python3
# .git/hooks/pre-commit
"""Pre-commit hook: Check for Python errors and formatting."""

import subprocess
import sys

def run_command(cmd):
    result = subprocess.run(
        cmd, shell=True, capture_output=True, text=True
    )
    if result.returncode != 0:
        print(result.stdout)
        print(result.stderr)
    return result.returncode

def main():
    errors = []

    # Check for syntax errors
    result = run_command("python -m py_compile $(git diff --cached --name-only | grep '\\.py$')")
    if result != 0:
        errors.append("Syntax errors found")

    # Check with flake8
    result = run_command("flake8 $(git diff --cached --name-only | grep '\\.py$')")
    if result != 0:
        errors.append("Style issues found (flake8)")

    if errors:
        print("โŒ Errors:", "; ".join(errors))
        sys.exit(1)

    print("โœ… Python checks passed")
    sys.exit(0)

if __name__ == "__main__":
    main()

Advanced: Python pre-commit Framework

# .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-json
      - id: check-added-large-files

  - repo: https://github.com/psf/black
    rev: 24.2.0
    hooks:
      - id: black

  - repo: https://github.com/commitizen-tools/commitizen
    rev: v3.13.0
    hooks:
      - id: commitizen
pip install pre-commit
pre-commit install

Testing Hooks

# Make the hook executable
chmod +x .git/hooks/pre-commit

# Skip a hook temporarily
git commit --no-verify -m "WIP: skip hooks"

# Skip all hooks
git commit -n -m "Emergency fix"

Team Setup Script

#!/bin/bash
# setup-hooks.sh โ€” run once per developer clone

echo "Setting up Git hooks..."

# Symlink shared hooks
HOOK_DIR=$(git rev-parse --show-toplevel)/.githooks
git config core.hooksPath .githooks

echo "โœ… Hooks configured. Files in .githooks/ are now active."

With core.hooksPath, hooks are tracked in your repo instead of .git/hooks/.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro