Skip to content

Hooks

One-liner

Git hooks are scripts that run automatically at specific points in the git workflow, enforcing quality gates locally.

Why it matters

Waiting 10 minutes for CI to catch a trailing whitespace, a failing test, or a malformed commit message is wasted time. Hooks move those checks to your local machine — catching problems in seconds, before code ever leaves. They reduce CI failures by 80-90% and create a faster feedback loop.

Core concept

Think of hooks like a doorman at a building entrance. Before you enter (commit) or leave (push), the doorman checks your ID (runs linting), inspects your bag (scans for secrets), and verifies your destination (validates commit message). If anything fails the check, you don't get through. The doorman works automatically — you don't have to remember to check yourself.

Hooks are executable scripts in .git/hooks/ that git calls at predefined trigger points. A non-zero exit code aborts the operation.

How it works

  1. Trigger — git reaches a lifecycle point (pre-commit, commit-msg, pre-push, etc.) and looks for a matching script in .git/hooks/.
  2. Execute — if the script exists and is executable, git runs it with relevant arguments (e.g., commit message file path).
  3. Gate — if the script exits with 0, the operation proceeds. Non-zero exit aborts the operation with an error message.
  4. Bypass — --no-verify skips pre-commit and commit-msg hooks (use sparingly).
  5. Share — since .git/hooks/ isn't tracked, teams store hooks in a tracked directory and configure core.hooksPath.

Key terms

  • pre-commit — runs before commit is created; ideal for linting, formatting, secret scanning
  • commit-msg — runs after you write the message; validates format and conventions
  • pre-push — runs before push transmits data; ideal for running test suites
  • core.hooksPath — git config that redirects hook lookup to a tracked directory (e.g., .githooks/)
  • --no-verify — flag to bypass hooks; should only be used for trivial changes
  • Exit code 125 — special in bisect context; in hooks, any non-zero code blocks the operation

Common misconceptions

  • "Hooks are optional fluff" — In a team setting, hooks enforce consistency that code review alone can't guarantee. They're infrastructure, not decoration.
  • "Hooks slow down my workflow" — Well-written hooks (checking only staged files, running in parallel) add under 2 seconds. Pre-push hooks can be slower but run less frequently.
  • "Everyone on the team automatically gets my hooks" — .git/hooks/ is local and untracked. You must explicitly share hooks via core.hooksPath, symlinks, or a setup script.

When to use / not use

✓ Use when: you want instant feedback on formatting/linting, need to enforce commit message conventions, want to run tests before pushing, or need to prevent accidental secret commits.

✗ Don't use when: the check requires network access you can't guarantee (use CI instead), the check takes more than 30 seconds for pre-commit (move it to pre-push), or the team hasn't agreed on the rules being enforced.

Example

# Set up shared hooks directory
git config core.hooksPath .githooks/

# Minimal pre-commit hook (lint staged Python files)
echo '#!/bin/bash
git diff --cached --name-only --diff-filter=ACM | grep "\.py$" | xargs ruff check' \
  > .githooks/pre-commit && chmod +x .githooks/pre-commit

Next steps

  1. Start with pre-commit (formatting + linting), add commit-msg (conventions), then graduate to pre-push (tests)
  2. Explore composable hook patterns — a dispatcher that runs all scripts in pre-commit.d/ directory
  3. Learn pre-commit framework (pre-commit.com) for language-agnostic, declarative hook management