Skip to main content

Command Palette

Search for a command to run...

Git Hooks: Automate Your Workflow Like a Pro

Learn: Git Hooks: Automate Your Workflow Like a Pro

Updated
10 min readView as Markdown
T

Welcome to TopperBlog! 👋

I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.

🎯 What I Write About: • AI/ML Engineering & LLMs • Web3 & Blockchain Development
• System Design & Architecture • Interview Preparation (FAANG) • Freelancing & Remote Work • Modern Tech Stacks (Next.js, React, Rust, TypeScript) • Performance Optimization & Best Practices

💼 Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.

📚 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.

🌐 Let's connect and grow together in this amazing tech journey!

#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering

Git Hooks: Automate Your Workflow Like a Pro (Pre-commit Magic Tricks)

You know that sinking feeling when you push code to production and immediately realize you left a console.log("WHY IS THIS BREAKING") in there? Or worse—you've committed your AWS credentials to a public repo and now some crypto miner in Belarus is having a field day with your credit card?

Yeah. We've all been there. And we've all sworn we'll be more careful next time.

Spoiler alert: You won't be. Because humans are terrible at repetitive tasks. We get tired, distracted, and overconfident. We need robots to save us from ourselves.

Enter Git hooks—your personal code bodyguard that stops you from doing stupid things before they become expensive things.

The "Oh Crap" Moment That Started It All

Picture this: It's 2 AM. You're three Red Bulls deep, finally fixing that bug that's been haunting you for days. You commit, you push, you close your laptop with the satisfaction of a job well done.

8 AM: Your Slack is on fire. The build is broken. Tests are failing. Your team lead is using the disappointed parent emoji. You forgot to run the linter. Again.

This was me, circa 2019, before I discovered Git hooks. Now? My Git repository literally won't let me commit garbage code. It's like having a really judgmental friend who checks your teeth for spinach before every meeting.

WTF Are Git Hooks Anyway?

Git hooks are scripts that Git executes automatically at specific points in your workflow. Think of them as event listeners for your repository—little programs that wake up and do stuff when you commit, push, merge, or perform other Git operations.

They live in the .git/hooks directory of your repository, lurking in the shadows, waiting to either save your ass or ruin your day (depending on how you configure them).

The most useful ones for daily development:

  • pre-commit: Runs before a commit is created (our star today)
  • pre-push: Runs before pushing to remote
  • commit-msg: Validates your commit messages
  • post-merge: Runs after a successful merge

Today, we're focusing on pre-commit hooks because they're the Swiss Army knife of preventing embarrassment.

Why Pre-commit Hooks Are Your New Best Friend

Here's the thing: Code review is great, but it's expensive. Every time a reviewer has to say "hey, you forgot to format this" or "um, there's a syntax error on line 47," that's time and mental energy wasted on stuff a robot could catch.

Pre-commit hooks are like spell-check for code. They catch the dumb stuff so humans can focus on the smart stuff—architecture decisions, logic errors, and whether your variable name thingy2 is really the best you can do.

Real benefits:

  1. Consistency: Everyone's code looks the same (no more tabs vs. spaces holy wars)
  2. Security: Catch secrets before they hit the repo
  3. Quality: Enforce linting, testing, and formatting standards
  4. Speed: Catch issues in seconds, not hours later in CI
  5. Sanity: Sleep better knowing you can't accidentally commit TODO: FIX THIS HORRIBLE HACK

The Manual Way (AKA The Painful Way)

Let's start with a basic pre-commit hook the old-school way. This will help you understand what's happening under the hood before we get fancy.

Navigate to your repo and create a hook:

cd your-awesome-project
cd .git/hooks
touch pre-commit
chmod +x pre-commit

Now edit that pre-commit file:

#!/bin/bash

echo "🔍 Running pre-commit checks..."

# Check for console.log statements
if git diff --cached --name-only | grep -E '\.(js|jsx|ts|tsx)$' | xargs grep -n "console\.log" 2>/dev/null; then
    echo "❌ ERROR: Found console.log statements. Remove them before committing."
    exit 1
fi

# Check for TODO comments
if git diff --cached | grep -E "TODO|FIXME" 2>/dev/null; then
    echo "⚠️  WARNING: Found TODO/FIXME comments. Are you sure you want to commit?"
    read -p "Continue? (y/n) " -n 1 -r
    echo
    if [[ ! $REPLY =~ ^[Yy]$ ]]; then
        exit 1
    fi
fi

echo "✅ All checks passed!"
exit 0

Try to commit something with console.log now. Go ahead. I'll wait.

Boom. Rejected. Your hook just saved you from yourself.

The problem with this approach? This hook lives in .git/hooks, which isn't tracked by Git. So every developer on your team needs to manually set this up. And we both know that's not happening.

The Pro Way: Enter pre-commit Framework

The pre-commit framework (yes, confusingly named the same as the hook type) is a Python tool that makes managing hooks actually pleasant. It's like the difference between writing raw SQL and using an ORM.

Installation

# Using pip
pip install pre-commit

# Or using Homebrew (macOS)
brew install pre-commit

# Or using conda
conda install -c conda-forge pre-commit

Setting Up Your First Config

Create a .pre-commit-config.yaml file in your repo root:

repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.5.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
      - id: check-added-large-files
        args: ['--maxkb=500']
      - id: check-json
      - id: check-merge-conflict
      - id: detect-private-key

Install the hooks:

pre-commit install

That's it. Now every developer who clones your repo just runs pre-commit install once, and they're protected.

Real-World Magic Tricks

Trick #1: Never Commit Secrets Again

repos:
  - repo: https://github.com/Yelp/detect-secrets
    rev: v1.4.0
    hooks:
      - id: detect-secrets
        args: ['--baseline', '.secrets.baseline']

This hook scans for API keys, passwords, tokens—basically anything that looks like it could cost you money if it leaks.

Pro tip: Generate a baseline file for known false positives:

detect-secrets scan > .secrets.baseline

Trick #2: Automatic Code Formatting

Stop arguing about code style. Let robots handle it.

For Python:

  - repo: https://github.com/psf/black
    rev: 23.12.1
    hooks:
      - id: black
        language_version: python3.11

For JavaScript/TypeScript:

  - repo: https://github.com/pre-commit/mirrors-prettier
    rev: v3.1.0
    hooks:
      - id: prettier
        types_or: [javascript, jsx, ts, tsx, json, css, markdown]

Now your code gets auto-formatted before every commit. No more "can you reformat this?" comments in PRs.

Trick #3: Enforce Commit Message Standards

  - repo: https://github.com/commitizen-tools/commitizen
    rev: v3.13.0
    hooks:
      - id: commitizen
        stages: [commit-msg]

This enforces conventional commit messages like:

feat: add user authentication
fix: resolve memory leak in data processor
docs: update API documentation

Why? Because good commit messages make git log actually useful, and they enable automatic changelog generation.

Trick #4: Run Your Tests

  - repo: local
    hooks:
      - id: pytest
        name: pytest
        entry: pytest
        language: system
        pass_filenames: false
        always_run: true

Warning: This can slow down commits if your test suite is large. Consider running only fast unit tests in pre-commit and saving integration tests for CI.

Trick #5: Lint Everything

Python:

  - repo: https://github.com/pycqa/flake8
    rev: 7.0.0
    hooks:
      - id: flake8
        args: [--max-line-length=100]

JavaScript/TypeScript:

  - repo: https://github.com/pre-commit/mirrors-eslint
    rev: v8.56.0
    hooks:
      - id: eslint
        files: \.[jt]sx?$
        types: [file]

The "Oh Shit" Escape Hatch

Sometimes you need to commit something that breaks the rules. Maybe you're in the middle of a refactor, or you're committing a known issue with a ticket to fix it.

# Skip all hooks for this commit
git commit --no-verify -m "WIP: refactoring in progress"

# Or skip specific hooks
SKIP=flake8,black git commit -m "temporary commit"

Use this power responsibly. With great power comes great "why did you commit that?"

Advanced: Custom Hooks for Your Specific Needs

Let's say you want to prevent commits to main branch (because we've all accidentally done that):

  - repo: local
    hooks:
      - id: prevent-main-commit
        name: Prevent commits to main
        entry: bash -c 'if [ "$(git rev-parse --abbrev-ref HEAD)" = "main" ]; then echo "❌ Cannot commit directly to main!"; exit 1; fi'
        language: system
        pass_filenames: false
        always_run: true

Or check for debugging statements:

  - repo: local
    hooks:
      - id: no-debug-statements
        name: Check for debug statements
        entry: bash -c 'git diff --cached | grep -E "(debugger|console\.log|print\(|pdb\.set_trace)" && exit 1 || exit 0'
        language: system

Performance: When Hooks Become Annoying

Here's the uncomfortable truth: If your pre-commit hooks take 30 seconds to run, developers will start using --no-verify. I've seen it happen.

Optimization strategies:

  1. Only check changed files: Most hooks do this by default
  2. Run expensive checks in CI: Save integration tests and full builds for your CI pipeline
  3. Use caching: Tools like black and eslint cache results
  4. Parallelize: The pre-commit framework runs hooks in parallel when possible
# Example: Only run on changed files
  - repo: https://github.com/psf/black
    rev: 23.12.1
    hooks:
      - id: black
        # This is default behavior, but you can be explicit
        pass_filenames: true

Team Adoption: Getting Everyone On Board

The technical part is easy. The human part? That's where things get interesting.

The wrong way: "Hey team, I added pre-commit hooks. They'll reject your commits if your code isn't perfect. Deal with it."

The right way:

  1. Start small: Add non-blocking hooks first (formatters, trailing whitespace)
  2. Communicate: Explain why these help everyone
  3. Make it easy: Document installation in your README
  4. Be flexible: Allow --no-verify for legitimate cases
  5. Iterate: Add stricter hooks gradually as the team adapts

Sample README section:

## Development Setup

After cloning this repo:

1. Install pre-commit: `pip install pre-commit`
2. Install hooks: `pre-commit install`
3. (Optional) Run on all files: `pre-commit run --all-files`

The hooks will automatically format your code and catch common issues.
If you need to bypass them temporarily, use `git commit --no-verify`.

The Complete Starter Config

Here's a battle-tested configuration that works for most projects:

# .pre-commit-config.yaml
repos:
  # General checks
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.5.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
      - id: check-added-large-files
        args: ['--maxkb=1000']
      - id: check-json
      - id: check-merge-conflict
      - id: detect-private-key
      - id: mixed-line-ending

  # Security
  - repo: https://github.com/Yelp/detect-secrets
    rev: v1.4.0
    hooks:
      - id: detect-secrets
        args: ['--baseline', '.secrets.baseline']

  # Python
  - repo: https://github.com/psf/black
    rev: 23.12.1
    hooks:
      - id: black

  - repo: https://github.com/pycqa/flake8
    rev: 7.0.0
    hooks:
      - id: flake8
        args: [--max-line-length=100, --extend-ignore=E203]

  # JavaScript/TypeScript
  - repo: https://github.com/pre-commit/mirrors-prettier
    rev: v3.1.0
    hooks:
      - id: prettier
        types_or: [javascript, jsx, ts, tsx, json, css, markdown]

  - repo: https://github.com/pre-commit/mirrors-eslint
    rev: v8.56.0
    hooks:
      - id: eslint
        files: \.[jt]sx?$
        types: [file]

  # Commit messages
  - repo: https://github.com/commitizen-tools/commitizen
    rev: v3.13.0
    hooks:
      - id: commitizen
        stages: [commit-msg]

Troubleshooting: When Hooks Go Wrong

"My hooks aren't running!"

# Check if hooks are installed
ls -la .git/hooks/pre-commit

# Reinstall
pre-commit uninstall
pre-commit install

"Hook X is failing but I don't know why"

# Run hooks manually with verbose output
pre-commit run --all-files --verbose

"I updated the config but nothing changed"

# Update hook versions
pre-commit autoupdate

# Clean and reinstall
pre-commit clean
pre-commit install

"Hooks are too slow!"

# See what's taking time
time pre-commit run --all-files

# Consider moving slow checks to CI

The Philosophical Bit: Automation vs. Autonomy

Here's a hot take: Some developers hate pre-commit hooks. They feel like Big Brother watching over their shoulder, preventing them from committing "work in progress" code.

And you know what? They have a point.

The key is balance. Hooks should catch obvious mistakes, not enforce opinions. They should make life easier, not harder.

Good hook: Prevents committing AWS credentials Bad hook: Rejects commits because a function is 51 lines instead of 50

Good hook: Auto-formats code to team standards Bad hook: Rejects commits for subjective style preferences

The goal is to automate the boring stuff so humans can focus on the interesting stuff. If your hooks are causing more frustration than they're preventing, dial them back.

Your Action Plan

Here's what you should do right now (yes, right now):

  1. Install pre-commit: pip install pre-commit or brew install pre-commit

  2. Add a basic config: Start with the starter config above, remove what you don't need

  3. Install hooks: pre-commit install

  4. Test it: pre-commit run --all-files

  5. Commit the config: Add .pre-commit-config.yaml to your repo

  6. Update your README: Tell your team how to set it up

  7. Iterate: Add more hooks as you discover pain points

The Bottom Line

Git hooks won't make you a better developer. But they will make you a more consistent developer. They'll catch your mistakes when you're tired, distracted, or just having an off day.

They're not about being perfect—they're about having a safety net.

And honestly? In a world where a single committed API key can cost you thousands of dollars, or a broken build can block your entire team, that safety net is worth its weight in gold.

So stop relying on your future self to remember to run the linter. Your future self is an idiot. Automate that shit.

Your team will thank you. Your code reviewers will thank you. And most importantly, your 2 AM self will thank you when the hook catches that console.log before it becomes a 8 AM Slack disaster.

Now go forth and hook responsibly. 🪝


Resources:

P.S. - If you found this helpful, the best way to thank me is to actually implement this in your next project. And maybe buy your DevOps team a coffee. They've been dealing with your unformatted commits for too long.