Command Palette

Search for a command to run...

Hectal
PHASE 4Intermediate ~14 min· topic 4 of 4

Topic 4.4

Git Hooks & Automated Checks

In one line

A Git hook is a script that runs automatically at a specific point in Git's workflow — the standard way to enforce a check (formatting, tests, commit message format) before a commit or push ever happens, without relying on anyone remembering to run it manually.

0/4 · 0%

Think of it like this

A car that physically won't start until every passenger has buckled their seatbelt — the check happens automatically, every single time, without relying on anyone remembering to do it themselves. A Git hook enforces exactly this kind of automatic, unskippable-by-accident check at a specific point in the Git workflow.

Key ideas

  1. 01

    Git hooks are scripts living in a repository's .git/hooks/ folder, each named after the exact event that triggers it — pre-commit runs right before a commit is finalized, commit-msg runs after the message is written (and can validate or reject its format), pre-push runs right before a push leaves your machine. If a hook script exits with a non-zero status, Git aborts that operation entirely.

  2. 02

    A pre-commit hook is the standard place to run fast, local checks: a code formatter, a linter, or a quick subset of tests — catching an obvious problem (unformatted code, an obvious lint error) before it's even committed, rather than discovering it later in CI or, worse, in review.

  3. 03

    Native .git/hooks/ scripts are NOT tracked by Git itself (they live outside the repository's actual tracked content) and don't get shared automatically when someone clones the repo — this is exactly the gap that tools like Husky (JavaScript) or pre-commit (Python, despite the confusingly similar name to the hook type itself) exist to solve: they let hook configuration be COMMITTED to the repository and automatically installed for every contributor.

  4. 04

    A commit-msg hook is the standard place to enforce a commit message FORMAT convention (Phase 7 covers writing genuinely good messages) — for example, requiring every message to start with a ticket number or a Conventional Commits prefix (feat:, fix:, docs:) — rejecting the commit outright if the format doesn't match, before it ever enters history.

  5. 05

    Hooks are a genuinely useful LAST LINE of local defense, but they're not a substitute for CI (Phase 7) — a hook can always be bypassed locally (git commit --no-verify skips hooks entirely, or someone simply hasn't installed the shared hook config yet), while CI runs on the SERVER, on every push, for every contributor, with no way to skip it short of the project's own configuration allowing it.

Code & diagrams

native-pre-commit-hook.shmarkdown

A raw native hook — instructive to see once, though most real teams use a shared tool instead (below).

# Native hooks live in .git/hooks/ and are NOT committed to the repo
cat > .git/hooks/pre-commit << 'EOF'
#!/bin/sh
echo "Running pre-commit checks..."

# Example: refuse to commit if a debug statement is still present
if git diff --cached | grep -q "console.log"; then
  echo "ERROR: found a console.log in staged changes — remove it first."
  exit 1
fi

exit 0
EOF
chmod +x .git/hooks/pre-commit

# Test it
echo "console.log('debug')" >> app.js
git add app.js
git commit -m "Add feature"
# ERROR: found a console.log in staged changes — remove it first.
# (the commit is blocked, exit code was non-zero)

# Bypass a hook deliberately, when genuinely justified (rare)
git commit -m "Emergency fix" --no-verify
pre-commit-framework.yamlmarkdown

The shared, committed alternative — every contributor gets identical hooks after one setup command.

# .pre-commit-config.yaml — committed to the repo, so it's shared automatically
repos:
  - repo: https://github.com/psf/black
    rev: 24.1.0
    hooks:
      - id: black
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.2.0
    hooks:
      - id: ruff

# Each contributor runs this ONCE after cloning:
# pip install pre-commit && pre-commit install
# From then on, black + ruff run automatically before every commit,
# identically for every contributor, with zero per-machine hook scripting.

Explain it without notes

01

Why don't native .git/hooks/ scripts automatically apply for a teammate who clones the same repository?

02

Why are Git hooks not a full substitute for CI, even though both can run tests and checks?

Practice

01

Write a simple native pre-commit hook that blocks a commit if a specific string (like a debug marker) is present in the staged diff, and confirm it correctly blocks and allows commits as expected.

02

If you use Node or Python regularly, set up Husky or the pre-commit framework on a real project and confirm a fresh clone (or a teammate) gets the same hooks automatically after the one-time install step.

Trade-offs

  • ↔

    Hooks catch problems fast, locally, before a commit or push even happens — genuinely valuable for tight feedback loops — but they add a small amount of friction to every commit (running a formatter or a subset of tests takes real time, however brief) and can always be bypassed, which is exactly why they're best understood as a fast, convenient first line of defense that complements CI, not a replacement for it.

Done when you can

  • I understand what a Git hook is and can name at least two common hook types (pre-commit, commit-msg).

  • I know why native .git/hooks/ scripts aren't shared automatically via clone.

  • I understand why hooks complement CI rather than replacing it.