Skip to content
Home
Git Hooks: Automate Your Workflow with Git Hooks

Git Hooks: Automate Your Workflow with Git Hooks

Git Git 7 min read 1454 words Beginner ExcellentWiki Editorial Team

Git hooks are scripts that run automatically at specific points in the Git workflow. They let you enforce policies, run checks, and automate tasks before or after commits, pushes, merges, and other Git events. Hooks are stored in the .git/hooks/ directory and are local to each repository — they are not pushed with the code.

How Hooks Work

Each hook is an executable script (bash, Python, Ruby, or any language) placed in .git/hooks/ with a specific name:

.git/hooks/
├── applypatch-msg
├── commit-msg
├── fsmonitor-watchman
├── post-commit
├── post-checkout
├── post-merge
├── post-rewrite
├── pre-applypatch
├── pre-commit
├── pre-merge-commit
├── pre-push
├── pre-rebase
├── pre-receive
├── prepare-commit-msg
├── push-to-checkout
├── receive-deny-current-branch
└── update

Git runs the hook script if it exists and is executable. If the script exits with a non-zero status, Git aborts the operation. This makes hooks a powerful enforcement mechanism — developers cannot bypass them without explicitly using --no-verify.

Common Client-Side Hooks

pre-commit

Runs before a commit is created. Use it to lint code, run tests, check for secrets, or enforce formatting.

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

echo "Running linter..."
npx eslint src/

if [ $? -ne 0 ]; then
    echo "Linting failed. Commit aborted."
    exit 1
fi

echo "Running tests..."
npm test -- --watchAll=false --passWithNoTests

if [ $? -ne 0 ]; then
    echo "Tests failed. Commit aborted."
    exit 1
fi

# Prevent committing to main branch
branch=$(git rev-parse --abbrev-ref HEAD)
if [ "$branch" = "main" ]; then
    echo "Direct commits to main are not allowed."
    exit 1
fi

commit-msg

Validates the commit message format before the commit completes:

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

# Enforce conventional commit format
pattern="^(feat|fix|docs|style|refactor|test|chore|perf|ci|build)(\(.+\))?: .+"
commit_message=$(cat "$1")

if ! [[ "$commit_message" =~ $pattern ]]; then
    echo "Error: Commit message must follow conventional commit format:"
    echo "  <type>(<scope>): <description>"
    echo "  Types: feat, fix, docs, style, refactor, test, chore, perf, ci, build"
    exit 1
fi

# Enforce max line length
while IFS= read -r line; do
    if [ ${#line} -gt 72 ]; then
        echo "Error: Commit message lines must be under 72 characters."
        echo "Line: $line"
        exit 1
    fi
done < "$1"

pre-push

Runs before a push is sent to the remote. Use it for final validation or blocking pushes to protected branches:

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

# Block force pushes
if [ "$GIT_PUSH_FORCE" = "true" ]; then
    echo "Force pushes are not allowed."
    exit 1
fi

# Run integration tests before push
echo "Running integration tests..."
npm run test:integration

if [ $? -ne 0 ]; then
    echo "Integration tests failed. Push aborted."
    exit 1
fi

prepare-commit-msg

Modifies the commit message before the editor opens. Useful for adding issue numbers or templates:

#!/bin/bash
# .git/hooks/prepare-commit-msg
# Prepend issue number from branch name to commit message

branch=$(git rev-parse --abbrev-ref HEAD)
issue=$(echo "$branch" | grep -oP '(?<=feature/)\d+' || echo "")

if [ -n "$issue" ]; then
    sed -i "1s/^/[$issue] /" "$1"
fi

Common Server-Side Hooks

Server-side hooks run on the remote repository (e.g., GitHub, GitLab, or a self-hosted Git server). They enforce policies across all contributors.

pre-receive

Runs on the server before any reference is updated:

#!/bin/bash
# hooks/pre-receive

while read oldrev newrev refname; do
    # Block tags that don't follow semver
    if [[ $refname =~ ^refs/tags/ ]]; then
        if ! [[ $refname =~ ^refs/tags/v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
            echo "Error: Tags must follow semver format (v1.2.3)"
            exit 1
        fi
    fi
done

update

Similar to pre-receive but runs once per reference being updated:

#!/bin/bash
# hooks/update

refname="$1"
newrev="$3"

# Enforce signed commits
if ! git verify-commit "$newrev" &>/dev/null; then
    echo "Error: Commit $newrev is not signed"
    exit 1
fi

Managing Hooks

Making Hooks Executable

chmod +x .git/hooks/pre-commit
chmod +x .git/hooks/commit-msg

Bypassing Hooks

git commit --no-verify       # Skip pre-commit and commit-msg
git push --no-verify         # Skip pre-push

Use these sparingly — they should be reserved for emergencies.

Pre-commit Framework

The pre-commit framework provides a standardized way to manage and share hooks. It uses a configuration file and installs hooks from a curated collection.

Installation

pip install pre-commit

Configuration

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
      - id: check-added-large-files
      - id: detect-private-key

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

  - repo: https://github.com/commitizen-tools/commitizen
    rev: v3.27.0
    hooks:
      - id: commitizen
        stages: [commit-msg]

  - repo: https://github.com/golangci/golangci-lint
    rev: v1.58.0
    hooks:
      - id: golangci-lint

Install

pre-commit install                # Install pre-commit hook
pre-commit install --hook-type commit-msg  # Install commit-msg hook
pre-commit run --all-files        # Run all hooks on all files
pre-commit autoupdate             # Update hook versions

The pre-commit framework solves the distribution problem that has historically plagued Git hooks. Instead of every developer manually copying hook scripts, a single config file checked into the repository defines exactly which hooks run. The framework downloads and manages hook versions automatically.

Custom Hook Templates

Share hooks across your team by storing them in a directory and configuring Git to use them:

mkdir -p .githooks
cp pre-commit .githooks/pre-commit
chmod +x .githooks/pre-commit

git config core.hooksPath .githooks

Commit the .githooks/ directory and every clone will use the same hooks automatically. This approach works well for teams that cannot or do not want to use the pre-commit framework.

Server-Side Hook Deployment

Deploying server-side hooks requires access to the Git server’s filesystem. For GitHub and GitLab, use their built-in protected branch rules and CI/CD integrations instead. For self-hosted Git servers, place hooks in the bare repository’s hooks/ directory:

# For a bare repository at /srv/git/project.git
cp pre-receive /srv/git/project.git/hooks/pre-receive
chmod +x /srv/git/project.git/hooks/pre-receive

Test server-side hooks by pushing to a test branch before enforcing them on main.

Hook Debugging Tips

Debugging hooks can be challenging because they run in a non-interactive environment. Here are practical techniques:

# Enable verbose output
set -x  # at the top of your hook script

# Redirect output to a log file for inspection
exec 1>/tmp/hook-debug.log 2>&1

# Check environment variables available to hooks
echo "GIT_DIR=$GIT_DIR" >> /tmp/hook-debug.log
echo "GIT_INDEX_FILE=$GIT_INDEX_FILE" >> /tmp/hook-debug.log

Common hook debugging steps:

  1. Check that the hook file is executable (ls -la .git/hooks/pre-commit)
  2. Run the hook manually: .git/hooks/pre-commit
  3. Check the shebang line matches the interpreter you expect
  4. Test with git commit --no-verify to confirm the hook is what blocks the operation

Best Practices

  • Keep hooks fast — developers will bypass slow hooks
  • Use stage-specific hooks — run fast checks in pre-commit, slower checks in pre-push
  • Don’t modify committed files — use git stash if you need to make changes
  • Provide clear error messages — developers need to know why a hook failed
  • Use the pre-commit framework — it handles installation, updates, and cross-platform compatibility
  • Store hooks in version control — use .githooks/ or a hooks/ directory
  • Document your hooks — explain what each hook does in your project README

Hook Security Considerations

Hooks that modify files or run external commands can be a security vector. The pre-commit framework mitigates this by pinning hook versions to specific commits rather than branch heads. If you write custom hooks, avoid downloading and executing code from the internet. For team environments, use the pre-commit framework’s verified hooks or maintain your own curated hook repository.

Conclusion

Git hooks automate your development workflow by running scripts at key points in the Git lifecycle. Use pre-commit for fast validation (linting, formatting), commit-msg for message format enforcement, and pre-push for integration tests. The pre-commit framework simplifies hook management and provides access to hundreds of community-maintained hooks. With a well-designed hook system, your team can catch issues before they reach code review, enforce consistent coding standards, and reduce the burden on human reviewers.

Related: See our Git workflow strategies and Git branching strategy.

FAQ

Q: Are Git hooks pushed with the repository? A: No. Hooks in .git/hooks/ are local and not tracked by Git. To share hooks, use a .githooks/ directory with core.hooksPath or use the pre-commit framework.

Q: What happens when a hook fails? A: Git aborts the operation. For example, if pre-commit exits with a non-zero status, the commit is not created. You can bypass hooks with --no-verify.

Q: What languages can I use for Git hooks? A: Any executable language — bash, Python, Ruby, Node.js, Go binaries, or compiled C programs. The hook file just needs to be executable.

Q: How do I debug a failing hook? A: Add set -x at the top of your bash hook script to enable verbose output. You can also run the hook manually: .git/hooks/pre-commit.

Q: Can I have multiple hooks for the same event? A: Not directly — there is only one script per event name. However, the pre-commit framework runs multiple hooks from within the single pre-commit script. Alternatively, your hook script can call multiple sub-scripts.

Q: What is the difference between client-side and server-side hooks? A: Client-side hooks run on each developer’s machine (pre-commit, pre-push). Server-side hooks run on the Git server (pre-receive, update) and enforce policies across all contributors.

Section: Git 1454 words 7 min read Beginner 756 articles in section Report inaccuracy Back to top