Skip to content
Pipelines and Pizza 🍕
Go back

Git Hooks: Automate Code Quality Before It Hits the Repo

5 min read

Your branching strategy is solid. Your team knows git rebase from git merge. Now what?

The difference between a team that uses Git and a team that leverages Git often comes down to hooks - scripts that catch problems before they spread.


Why This Matters for Infrastructure Engineers

DevOps lives at the intersection of code and operations. Git can do more than track changes - it can enforce standards automatically.

I once spent an hour reviewing a junior engineer’s Terraform PR that should have taken twenty minutes. The logic was solid - they’d done good work - but it was hard to follow. Inconsistent indentation, tabs mixed with spaces, resource blocks formatted differently depending on which example they’d referenced.

Instead of reviewing infrastructure logic, I left a dozen comments about formatting. That’s not the feedback anyone wants to give or receive. The engineer had put real effort into the PR, and we ended up spending our sync talking about whitespace instead of their actual contribution.

The fix wasn’t more code review diligence - it was a pre-commit hook. Now terraform fmt runs automatically before any commit reaches the repo. The code is always readable, reviews focus on what matters, and new engineers get consistent formatting from day one without anyone having to point it out.


What Are Git Hooks?

Hooks are scripts Git runs on specific events. They’re your first line of defense.

Local vs Server-Side

  • Local hooks live in .git/hooks/ (not versioned by default)
  • Server-side hooks run on the remote (pre-receive, update) - managed by platform admins

The hooks you’ll use most:

HookWhen it runsUse case
pre-commitBefore commit is createdLint, format, block secrets
commit-msgAfter message is writtenEnforce conventional commits
pre-pushBefore push to remoteFast validation
post-mergeAfter merge completesSync dependencies

Pre-Commit: Quality at the Edge

A pre-commit hook prevents garbage from entering your history. Here’s one that handles Terraform formatting and catches secrets:

#!/usr/bin/env bash
set -euo pipefail

echo "[pre-commit] running checks..."

STAGED=$(git diff --cached --name-only --diff-filter=ACM)

# Terraform fmt on staged *.tf files
echo "$STAGED" | grep -E '\.tf$' >/dev/null && {
  terraform fmt -recursive
  git add $(echo "$STAGED" | grep -E '\.tf$' || true)
}

# YAML lint
if command -v yamllint >/dev/null; then
  echo "$STAGED" | grep -E '\.ya?ml$' >/dev/null && \
    yamllint -s $(echo "$STAGED" | grep -E '\.ya?ml$' || true)
fi

# Block secrets (basic pattern matching)
if echo "$STAGED" | xargs grep -E 'AKIA[0-9A-Z]{16}|secret_key|password\s*:' 2>/dev/null; then
  echo "Potential secret detected. Aborting."
  exit 1
fi

echo "[pre-commit] OK"

The Better Way: pre-commit Framework

Writing raw hook scripts works, but the pre-commit framework makes it maintainable:

pip install pre-commit

.pre-commit-config.yaml:

repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
  rev: v6.0.0
  hooks:
  - id: trailing-whitespace
  - id: end-of-file-fixer
  - id: check-yaml
- repo: https://github.com/antonbabenko/pre-commit-terraform
  rev: v1.105.0
  hooks:
  - id: terraform_fmt
  - id: terraform_validate
pre-commit install

Now every commit runs through these checks automatically.


Commit Message Hooks: Enforce Conventions

Conventional Commits (feat:, fix:, chore:) let CI generate changelogs and trigger release logic. Enforce them:

.git/hooks/commit-msg:

#!/usr/bin/env bash
set -euo pipefail
MSG_FILE="$1"

PATTERN='^(feat|fix|build|docs|style|refactor|perf|test|ci|chore|revert)(\(.+\))?: .{1,80}$'

if ! grep -Eq "$PATTERN" "$MSG_FILE"; then
  echo "Commit message must follow Conventional Commits:"
  echo "  feat(api): add rate limiter"
  echo "  fix(terraform): correct storage account SKU"
  exit 1
fi

Pre-Push: One Last Check

Run fast validations before code leaves your machine. Keep it under 10 seconds or people will bypass it.

#!/usr/bin/env bash
set -euo pipefail

# Check if upstream is set; skip diff if not
if ! git rev-parse --abbrev-ref '@{u}' &>/dev/null; then
  echo "[pre-push] No upstream set, skipping diff check"
  exit 0
fi

FOUND=$(git diff --name-only @{u}..HEAD | grep -E '\.tf$' || true)
if [ -n "$FOUND" ]; then
  terraform fmt -check -recursive || exit 1
fi

echo "[pre-push] OK"

Post-Merge: Keep Environments Fresh

After pulling or merging, refresh dependencies automatically:

#!/usr/bin/env bash
set -euo pipefail

[ -f requirements.txt ] && pip install -r requirements.txt >/dev/null || true
[ -f package.json ] && npm ci --prefer-offline >/dev/null || true
[ -d .gitmodules ] && git submodule update --init --recursive

echo "[post-merge] deps refreshed"

Hands-On Lab: Set Up Git Hooks

Step 1: Initialize a repo

mkdir git-hooks-lab && cd git-hooks-lab
git init
echo "# Git Hooks Lab" > README.md
git add README.md && git commit -m "chore: init"

Step 2: Install pre-commit hooks

pip install pre-commit

cat > .pre-commit-config.yaml <<'YAML'
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
  rev: v6.0.0
  hooks:
  - id: trailing-whitespace
  - id: end-of-file-fixer
  - id: check-yaml
- repo: https://github.com/antonbabenko/pre-commit-terraform
  rev: v1.105.0
  hooks:
  - id: terraform_fmt
  - id: terraform_validate
YAML

pre-commit install

Step 3: Add commit message enforcement

cat > .git/hooks/commit-msg <<'HOOK'
#!/usr/bin/env bash
set -euo pipefail
PATTERN='^(feat|fix|build|docs|style|refactor|perf|test|ci|chore|revert)(\(.+\))?: .{1,80}$'
if ! grep -Eq "$PATTERN" "$1"; then
  echo "Commit message must follow Conventional Commits."
  exit 1
fi
HOOK
chmod +x .git/hooks/commit-msg

Troubleshooting Guide

ProblemCauseFix
Hook didn’t runNot executablechmod +x .git/hooks/pre-commit
Hooks not shared with team.git/hooks/ isn’t versionedUse pre-commit framework or bootstrap script

Quick Reference

pre-commit install              # Install pre-commit framework
chmod +x .git/hooks/pre-commit  # Make hook executable
pre-commit run --all-files      # Run hooks on all files
pre-commit autoupdate           # Update hook versions

What’s Next

Next post: Git Submodules - Managing Shared Code Across Repos. We’ll look at how to embed repositories within repositories, keep them in sync, and avoid the pitfalls that trip up every team.

Happy automating!