Skip to content
Go back

Git Hooks You Should Be Using Locally Right Now

By SumGuy 5 min read
Git Hooks You Should Be Using Locally Right Now

The Problem You Didn’t Know You Had

You just pushed code. Three minutes later, CI fails because you forgot to run tests. Or your code has a syntax error. Or you committed a debug statement. Or worse — a hardcoded password made it into the repo.

Git hooks fix this. They’re scripts that run before (or after) git operations. Locally. On your machine. No CI wait. No red build. Just instant feedback.

Most developers don’t use them. They should.

What Are Git Hooks?

Git hooks live in .git/hooks/ and run automatically when you do a git operation. The main ones you care about:

They’re just shell scripts. Nothing fancy.

The Setup

First, don’t edit files in .git/hooks/ directly — they don’t get committed. Instead, create a hooks/ directory in your repo root and commit it:

Terminal window
mkdir -p hooks
chmod +x hooks/*

Then configure git to use them:

Terminal window
git config core.hooksPath hooks

Done. Now .git/hooks/ symlinks to your committed hooks/ directory.

A Practical pre-commit Hook

Here’s what I use. Save as hooks/pre-commit:

#!/bin/bash
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m'
echo "Running pre-commit checks..."
# Check for hardcoded secrets
if git diff --cached | grep -E '(password|api_key|secret|token|AWS_SECRET)' -i; then
echo -e "${RED}Error: Potential secret detected in staged changes${NC}"
exit 1
fi
# No console.log in JavaScript
if git diff --cached -- '*.js' '*.ts' | grep -E '^\+.*console\.(log|debug|error)'; then
echo -e "${RED}Error: console.log detected. Remove before commit.${NC}"
exit 1
fi
# Run tests (example for Node.js)
if [ -f package.json ]; then
npm run test -- --bail 2>/dev/null || {
echo -e "${RED}Tests failed. Fix before committing.${NC}"
exit 1
}
fi
# Lint staged files
if command -v eslint &> /dev/null; then
eslint --fix $(git diff --cached --name-only -- '*.js' '*.ts') 2>/dev/null || true
fi
echo -e "${GREEN}Pre-commit checks passed!${NC}"
exit 0

Make it executable: chmod +x hooks/pre-commit

Now when you run git commit, it’ll:

  1. Search for hardcoded secrets
  2. Reject console.log statements
  3. Run tests
  4. Lint and auto-fix your code

If any check fails, the commit is blocked. Locally. No CI pain.

A pre-push Hook Example

Paranoia: validate that you’re pushing to the right branch and that tests still pass:

#!/bin/bash
set -e
BRANCH=$(git rev-parse --abbrev-ref HEAD)
# Don't push to main without a PR (enforce workflow)
if [[ "$BRANCH" == "main" ]]; then
echo "Cannot push directly to main. Use a PR."
exit 1
fi
echo "Running pre-push checks on $BRANCH..."
# Quick test
npm run test -- --bail || {
echo "Tests failed. Not pushing."
exit 1
}
echo "All checks passed. Pushing $BRANCH..."
exit 0

Save as hooks/pre-push and chmod +x it.

Common Hook Pitfalls

Hooks run in a weird environment. No bash aliases, no shell functions, no cd from previous shells. Use absolute paths and full command names.

Terminal window
# Bad—relies on alias
git_log
# Good—explicit
/usr/bin/git log

The —no-verify escape hatch. Anyone can bypass hooks with git commit --no-verify. It’s there for emergencies, not daily use. If people use it constantly, the hook isn’t solving the real problem.

Subshells and exit codes matter. If your hook fails silently, the commit goes through anyway:

Terminal window
# Bad—errors are swallowed
npm run test > /dev/null 2>&1
# Good—errors bubble up
npm run test || exit 1

A Sample Hook for Python Projects

#!/bin/bash
set -e
# Format with black
if command -v black &> /dev/null; then
black --quiet $(git diff --cached --name-only -- '*.py')
fi
# Check types with mypy
if command -v mypy &> /dev/null; then
mypy $(git diff --cached --name-only -- '*.py') || {
echo "Type check failed."
exit 1
}
fi
# No debug breakpoints
if git diff --cached -- '*.py' | grep -E '^\+.*(pdb|breakpoint|ipdb)'; then
echo "Debug statement detected. Remove before commit."
exit 1
fi
echo "Pre-commit checks passed!"
exit 0

Why You Should Care

Developer experience: Instant feedback beats waiting 5 minutes for CI.

Cost: Fewer CI runs = fewer credits spent. Fewer builds = faster feedback loops.

Team trust: You stop committing surprises.

Onboarding: New devs run git config core.hooksPath hooks once and inherit all your standards.

Real Talk

Hooks aren’t foolproof — you can always git commit --no-verify to bypass them. Don’t. If you’re tempted, ask yourself why the check exists.

Also: hooks run on your machine. They won’t catch issues that only appear in CI (dependency versions, environment-specific bugs). But they catch 80% of the stupid stuff before it leaves your laptop.

Start with pre-commit. Add pre-push when you’re paranoid. Invest 20 minutes now to save weeks of “why didn’t CI catch this?”

Your future self will thank you. Probably at 2 AM when you’re debugging instead of sleeping.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it may appear here.


Previous Post
Vault vs Infisical: Secrets Management for Teams Who've Learned the Hard Way
Next Post
Traefik: Docker Routing with Labels

Related Posts