Automate Git Workflows with Hooks

Learn how to automate workflows with git hooks in this hands-on Git Tutorial lesson. Covers the core concept, step-by-step walkthrough, and troubleshooting.

Focus: automate workflows with git hooks

Sponsored

You're mid-refactor, about to commit a breaking change, and you hit Enter. The commit lands, CI fails minutes later, and you've already pushed to a shared branch. The fix costs you twenty minutes and a rebase. Now imagine a safety net that runs your tests, checks your code style, or even rejects the commit before it ever lands. That's the power of git hooks — small scripts Git runs at key moments in your workflow. In this lesson, you'll learn how to automate workflows with git hooks, turning manual checks into automatic guardrails that save time and prevent costly mistakes.

The problem this lesson solves

Without hooks, you rely on discipline. You tell yourself you'll run the linter before committing, or remember to write a clear message. But discipline fades under deadline pressure. The result: sloppy commits, broken builds, and debug statements that sneak into production. The bigger the team, the worse the problem — one bad merge can stall everyone.

Git hooks solve this by moving checks into Git itself. Instead of hoping developers remember, you make Git run the checks automatically. No more chasing down "who committed the missing semicolon." The hook catches it before anyone sees it.

Pro tip: Hooks are local by default — they stay in your .git/hooks directory and aren't shared. That's perfect for personal workflows, but for team-wide rules you'll want to version them (more on that later).

Core concept / mental model

Think of GitHub Actions or a CI pipeline — but running before the event, on your machine. Hooks are scripts that execute at specific points in Git's lifecycle. They're like guardrails on a highway: you don't notice them until you're about to veer off course.

A mental model: Git is the librarian and hooks are the bookends. When you ask to add a book (commit), the bookend checks that it's the right size, has a cover, and isn't already on the shelf. If it fails, the book is rejected. You can't skip the check — you have to fix the book first.

Formally, a hook is an executable file in .git/hooks/ with a name like pre-commit, pre-push, or commit-msg. When Git reaches that event, it runs the script. The script's exit code decides the outcome:

  • Exit 0 → success, Git continues
  • Non-zero exit → failure, Git aborts the operation

That's the entire magic. You write a script, make it executable, and Git obeys.

How it works step by step

Let's trace a typical git commit through the hook lifecycle:

  1. You run git commit.
  2. Git executes pre-commit (if it exists).
  3. pre-commit runs your checks (linting, formatting, secret scanning).
  4. If the script exits 0, Git proceeds to create the commit.
  5. If it exits non-zero, the commit is blocked, and you see the script's output.
  6. After the commit, post-commit runs (for notifications, etc.).

Here's a visual in words:

pre-commit → commit-msg → post-commit
pre-push → post-update (on remote)
prepare-commit-msg → pre-merge-commit

Each hook is a placeholder for your script. The most useful ones:

  • pre-commit — lint, format, run unit tests, check for secrets.
  • prepare-commit-msg — auto-fill subject or add issue numbers.
  • commit-msg — enforce commit message conventions.
  • pre-push — run slow tests, build, or verify credentials.

Pro tip: You can have multiple checks inside one hook by chaining commands with && or using a hook manager like husky / pre-commit (the Python tool) to manage them declaratively.

Hands-on walkthrough

Let's build a practical pre-commit hook that rejects commits if there's a trailing whitespace or a marker like FIXME in the staged changes.

Step 1: Create a test repository

mkdir git-hooks-demo && cd git-hooks-demo
git init
echo "console.log('hello')" > app.py
git add app.py

Step 2: Write the hook

Create a file .git/hooks/pre-commit (no extension) with the following content:

#!/bin/bash
echo "Running pre-commit checks..."

# Check for trailing whitespace in staged files
if git diff --cached --check; then
    echo "Whitespace check passed"
else
    echo "ERROR: Trailing whitespace detected. Fix before committing."
    exit 1
fi

# Check for FIXME markers in staged lines
if git diff --cached | grep -i 'FIXME'; then
    echo "ERROR: FIXME marker found in staged changes."
    exit 1
fi

echo "All checks passed"

Make it executable:

chmod +x .git/hooks/pre-commit

Step 3: Test it

Add a line with trailing whitespace:

echo "bad line   " >> app.py
git add app.py
git commit -m "Introduce a bad line"

You'll see the hook block the commit:

Running pre-commit checks...
<line with trailing whitespace>
ERROR: Trailing whitespace detected. Fix before committing.

Now fix it:

sed -i 's/[[:space:]]*$//' app.py
git add app.py
git commit -m "Clean up whitespace"

This time the commit succeeds, and the hook reports success.

Step 4: Watch the exit code

Run echo $? after a failed commit — it returns 1, confirming the hook's blocking power.

Compare options / when to choose what

Hooks can be split into two worlds: client-side (your machine) and server-side (remote repository). Most hooks you'll write are client-side. Here's a comparison:

Hook type Where it runs Use cases Sharing
pre-commit Local Lint, format, unit tests Requires versioning setup
commit-msg Local Enforce message conventions (e.g., conventional commits) Requires versioning setup
pre-push Local Slow integration tests, build Requires versioning setup
pre-receive Server Reject pushes with secrets, enforce branch protection Automatically applied to all clones

When deciding where to place a check:

  • Fast checks (lint, whitespace) → pre-commit
  • Message validationcommit-msg
  • Slow tests (minutes) → pre-push to avoid blocking every commit
  • Security critical (secrets) → server-side pre-receive for ultimate enforcements

Pro tip: Don't overload pre-commit with slow tests — it'll frustrate your team. Save those for pre-push.

Troubleshooting & edge cases

  • Hook not running? Make sure the file is executable (chmod +x). Git silently skips non-executable hooks.
  • Exit code ignored? Some hooks (like post-commit) don't block anything — they don't affect success. Only the pre-* hooks block.
  • Bypass detection: You can skip hooks with git commit --no-verify. That's useful for emergencies, but teaches bad habits. Use it sparingly.
  • Too many hooks? Use a manager like husky (Node) or pre-commit (Python) to share hooks across a team. They version your scripts in the repo and install them automatically.
# Skip hooks for a one-off commit
git commit --no-verify -m "hotfix"

What you learned & what's next

You now know how to automate workflows with git hooks: the event model, exit-code protocol, and practical script writing. You can enforce standards before code ever reaches the remote. This is a small but powerful step toward mature Git workflows.

Next, you'll explore branch protection rules and code review — how to back up your hooks with server-side policies that even --no-verify can't bypass. That's where teams truly enforce quality.

To solidify your skills, try writing a commit-msg hook that enforces a specific prefix (like feat: or fix:) — a common real-world pattern.

Practice recap

Write a pre-commit hook that runs python -m py_compile on all staged .py files and blocks the commit if any fail. Then, intentionally add a syntax error, try to commit, and watch the hook catch it. After fixing, verify the commit succeeds. This gives you a taste of real-world guardrails before moving on to branch protection.

Common mistakes

  • Forgetting to chmod +x the hook file — Git silently ignores non-executable hooks, so nothing happens and checks are skipped.
  • Making the hook too slow (e.g., running full test suite in pre-commit) — it blocks every commit and leads to users using --no-verify out of frustration.
  • Writing output to stdout instead of stderr — Git may show it confusingly, and some hook managers capture only stderr.
  • Not handling staged files correctly — you must check git diff --cached (staged) content, not the working directory, otherwise uncommitted changes can be missed or false positives introduced.
  • Assuming hooks are shared by default — they're local, so your team needs a separate mechanism (like a hook manager or versioned scripts) to install them consistently.

Variations

  1. Use a hook manager like 'husky' (JavaScript) or 'pre-commit' (Python) to define and share hooks declaratively — they handle installation, versioning, and language-specific runners.
  2. Set up server-side hooks like pre-receive or update to enforce policies on the central repository — this prevents bypasses even with --no-verify.
  3. Combine hooks with CI/CD pipelines: use pre-push for fast local checks, then let CI run deeper tests after push; this balances speed and thoroughness.

Real-world use cases

  • Automatically lint and format staged files before every commit to keep the codebase clean.
  • Enforce conventional commit message formats (e.g., feat:, fix:) with a commit-msg hook to improve changelog generation.
  • Block pushes with accidental secrets or large files using a pre-push hook, preventing leakage to the remote.

Key takeaways

  • Hooks are local, executable scripts in .git/hooks/ that Git runs at specific events; the exit code (0 success, non-zero failure) determines if the operation continues.
  • The most impactful hooks are pre-commit, commit-msg, and pre-push — use them for fast checks and policy enforcement.
  • Check the staged content (git diff --cached) in hooks, not the working tree, to ensure you're validating what will actually be committed.
  • Make hooks executable and test them carefully — a missing chmod is the #1 reason hooks silently don't run.
  • For team-wide automation, use a hook manager or version the hook scripts to ensure every developer gets the same guards.
  • Use --no-verify only as a last-resort emergency bypass — overusing it defeats the entire purpose of hooks.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.