Stop Wasting CI Minutes - Use Git Hooks Instead
Let’s set the scene: you’re an hour away from a demo and need to merge multiple fixes. You want each fix to have its own commit so you can roll back easily if something goes south.
Now the issue is, you’re working in a monorepo with a full CI pipeline that needs to finish before you can merge your fixes. In those moments, you’re burning CI minutes - and losing time waiting for those pipelines to finish.
But there is another way! One where you’re in control, faster, and more efficient. In fact, most CI pipeline steps can be replaced with scripts that run locally - before you even push to the repository.
Let’s see how Git hooks can save you time, money, and spare you from CI setup frustration.
Using Your Local Machine for CI Tasks
Suppose you’re working on a TypeScript project with linting enabled, a few hundred tests, source map generation, and semantic versioning. Naturally, it needs dependencies installed and transpiling to JavaScript before it can even run.
While working on such a project, your local environment is already set up for all that. So why not leverage it to also run the checks and scripts that would otherwise be handled by a CI machine?
You can trigger all of that automatically using Git hooks.
To make setup easier, we’ll use Husky - a tool designed to simplify Git hook management.
Git Hooks and Husky
Git hook scripts, as the name suggests, can be set up to execute on various Git events. These events align perfectly with common CI triggers, such as pre-rebase, pre-push, pre-commit, etc.
We’ll focus on the pre-commit hook since it ties in nicely with CI flows that include safeguarding branches with code that has passed required checks.
Here’s an example script for a pre-commit Git hook that covers typical CI steps for a TypeScript project:
echo "[pre-commit hook] Running pre-commit hook. Linting, testing, checking build..."
npx xo
npm test
npm run build
echo "[pre-commit hook] Increase version?"
select prompt in "Major" "Minor" "Patch" "No"; do
case $prompt in
Major ) npm --no-git-tag-version version major; break;;
Minor ) npm --no-git-tag-version version minor; break;;
Patch ) npm --no-git-tag-version version patch; break;;
No ) exit;;
esac
done
The commit proceeds only if every command runs successfully - and you get to choose the version bump at the end. And since your package.json already has the scripts defined, there's no need to invent anything new.
What about setup time? Just a few minutes.
CI Runners Are Just Machines
When setting up a CI workflow, we often think of CI runners as clones of our local or staging environment. But they’re neither.
They are running a third, isolated environment - with its own:
- OS
- Networking
- File system
- Caching (or lack thereof)
- Node versions
- Dependency resolution
So if you rely on the CI workflow to catch issues that don’t happen locally, you’re trusting a completely different setup. That’s like testing in Safari and hoping it works in Chrome.
Waiting on a runner to execute your workflow isn’t just slow - it’s often unreliable, too.
Save Time and Resources
And it doesn’t matter which CI provider you use - most services are either limited or expensive, especially for monorepos, larger teams, or open-source projects.
By moving the “pre-flight” checks (lint, test, type, version etc.) to Git hooks:
- You don’t burn CI minutes on things you already know will pass
- You don’t need to replicate your full environment just to run basic scripts
- You fail fast - right in the terminal where you are already doing your work
- You save time: imagine skipping 10 minutes of CI because you caught the error locally in 5 seconds
Commit Conditions vs. Merge Conditions
Continuous integration typically functions as a merge condition, effectively asking:
“Is this code safe to merge into main?”
But Git hooks give you a commit condition:
“Is this code safe to save?”
By the time your branch hits the CI pipeline, it’s already clean. You’ve shifted the feedback loop closer to the developer - and closer to where issues can be fixed fastest.
Boiling It All Down
You might ask yourself: “Shouldn’t I still run tests in CI?”
The answer would be yes! Git hooks are ideal for fast, local feedback - for catching issues before the code ever leaves your machine. But CI still plays a critical role in your development lifecycle. The key is to use each tool where it shines:
Use Git Hooks for:
- Fast, local validation (e.g., linting, type checks, unit tests)
- Immediate feedback before the commit even happens
- Preventing broken code from entering the repo at all
This tight feedback loop makes Git hooks perfect for pre-commit or pre-push checks. They enforce quality early and reduce unnecessary CI runs caused by avoidable issues.
Use CI workflows for:
- Integration tests against external services:
- These are slower, more complex, and rely on shared infrastructure - not ideal for local runs.
- Building production-ready bundles:
- CI runners guarantee consistency across environments and catch build issues that might not show up locally.
- Deploying to preview environments:
- Automatically spinning up staging instances for branches or PRs allows for collaboration, review, and user testing before merging to main.
In short:
Let Git hooks guard the gates locally. Let CI workflows validate that everything still works when the whole team’s code comes together.
You might also like reading these blog posts:
How We Grow Together at LambdaWorks
Marko Katić
How ChatGPT Can Help Us in Writing Clear Bug Reports
Ognjen Stajčić