Look, I've been slinging code for longer than I care to admit, and one thing never fails to blow my mind: teams dropping serious cash on massive CI pipelines just to catch the dumbest mistakes imaginable.
We're out here pushing broken code on purpose, waiting for GitHub Actions or some cloud runner to slap our wrists and say "bad developer." It's like paying a bouncer to tell you your shoes are untied after you've already tripped down the stairs.
There's a better way. A way that's instant, local, free, and doesn't make you wait eight minutes to learn that ESLint is mad at you again.
It's called pre-commit hooks. And yeah, they're ancient. But they're also brutally effective.
What the Heck Is a Pre-Commit Hook, Anyway?
At its core, a pre-commit hook is just a script that Git runs right before it seals the deal on your commit.
- Script exits with code 0 → Commit goes through.
- Script exits with anything else → Commit aborted. Fix your mess.
No YAML hell. No spinning runners. No "let's push to main and see what breaks."
Just pure, local enforcement.
The Real Cost of "We'll Catch It in CI"
We've normalized this weird ritual where basic quality checks happen after the code's already in the repo. Here's what that actually costs you:
| Problem | Caught Where? | Real Cost |
|---|---|---|
| Formatting issues | CI | Minutes per PR, endless nitpicks |
| Linting failures | CI | Cloud minutes, context switches |
| Type errors | CI | Interrupted flow |
| Leaked secrets | CI | Potential breaches (too late!) |
| OpenAPI drift | CI | Broken contracts downstream |
| Stale generated code | CI | Rework and blame games |
Every single one of these can—and should—be caught in milliseconds on your machine.
CI is for verification.
Pre-commit is for prevention.
Shifting Left... Like, Way Left
Pre-commit hooks are the earliest possible gate in your entire development lifecycle. They run before:
- The PR exists
- CI spins up
- Reviews happen
- Anything deploys
- Incidents occur
You're enforcing standards at the exact moment your intent turns into code. That's power.
A Dead-Simple But Devastating Setup
Here's a minimal pre-commit hook that'll instantly level up your project. Drop this into .git/hooks/pre-commit:
#!/usr/bin/env bash
set -e
echo "🤠 Hold up, partner—running the gauntlet..."
npm run lint || { echo "🚨 Lint's not happy. Fix it."; exit 1; }
npm run format:check || { echo "🧹 Formatting's a disaster."; exit 1; }
npm run typecheck || { echo "🔥 Types are throwing hands."; exit 1; }
npm test -- --runTestsByPath $(git diff --cached --name-only) || { echo "🧪 Tests failed on changed files."; exit 1; }
echo "✅ All clear. Commit away, legend."
Make it executable:
chmod +x .git/hooks/pre-commit
Boom. You've now enforced:
- Linting
- Formatting
- Type safety
- Targeted tests on changed files
All for the low price of absolutely nothing.
Stop Committing Secrets Before They Even Exist
This one pays for itself the first time it saves your bacon.
Add this to your hook:
if git diff --cached | grep -E "(AWS_SECRET|API_KEY|PRIVATE_KEY|PASSWORD)"; then
echo "🚨 Whoa there—looks like a secret's trying to escape. Commit aborted."
exit 1
fi
CI detecting secrets post-commit? That's already game over. The commit exists. The damage is done.
The Sleeper Hit: Hooks as Architecture Enforcement
This is where pre-commit hooks go from "nice to have" to "how did we live without this?"
Use them to enforce contracts across your codebase:
- Touch
openapi.yaml? Auto-regenerate clients and fail if they're stale. - Add a migration without a down file? Nope.
- Bump version without changelog entry? Try again.
Example for OpenAPI:
if git diff --cached --name-only | grep openapi.yaml; then
echo "🔄 Regenerating clients..."
make generate-clients
if ! git diff --quiet; then
echo "❌ Generated clients are out of date. Stage them!"
exit 1
fi
fi
No more PR comments. No CI failures. No "wait, why is the client broken?"
Just correct-by-construction development.
Pre-Commit vs CI: It's Not Even Close
| Factor | Pre-Commit Hooks | CI Pipelines |
|---|---|---|
| Feedback speed | Instant | Minutes |
| Cost | Free | $$$ (cloud minutes) |
| Developer focus | Preserved | Interrupted |
| Enforcement | Hard stop | Advisory (often ignored) |
| Works offline | Yes | No |
CI says: "Hey, you messed up."
Pre-commit says: "No. Fix it right now."
"But Devs Can Bypass It With --no-verify!"
Sure. And the same person would've skipped CI checks too.
Hooks aren't about stopping determined chaos agents. They're about making the right path the default path for everyone else.
You can't accidentally bypass a hook. That's the point.
It's a Culture Thing
Rolling out pre-commit hooks sends a message:
- We respect your time
- We don't outsource basic discipline to cloud services
- We prevent problems instead of reacting to them
- We value correctness over vanity velocity metrics
That's mature engineering.
The Iron Law of Pre-Commit Hooks
If it's cheap to check and expensive to fix later, it belongs in a pre-commit hook.
Full stop.
Final Thoughts
We're all sleeping on pre-commit hooks because they're old, boring, and don't have a SaaS dashboard with pretty graphs.
But they work. They've always worked.
And they quietly save teams thousands in CI minutes, review cycles, incident response, and developer burnout.
Next time you're about to add another GitHub Action for something basic, stop.
Write a hook instead.
Your future self will thank you. Your teammates will thank you. Your cloud bill will thank you.
Now go forth and hook responsibly 🤠
Note: For larger teams, consider using the pre-commit framework—it manages hooks across repos and makes sharing configs easy. But even plain Git hooks are a massive win.
Top comments (0)