Pre-commit Hooks: Catching Mistakes Before They Reach the Repo
Stop the secret, the syntax error, and the unformatted file at the door

There is a particular flavour of shame that comes from pushing a commit, watching CI light up red ninety seconds later, and discovering the failure was a trailing-whitespace lint error or a file you forgot to format. Worse is the commit that ships an AWS key in a .env you meant to gitignore, now etched into history forever. Pre-commit hooks catch all of this at the only moment it’s cheap to catch: before the commit exists. Git has always supported hooks; the pre-commit framework just makes them sane to manage.
1 Why a framework, not a raw hook
Git’s native hooks live in .git/hooks/, which is not version-controlled and is therefore invisible to your team. You write a pre-commit shell script, it works on your machine, nobody else has it, and it bit-rots. The pre-commit framework (a Python tool, despite working on any language’s repo) fixes the distribution problem: hooks are declared in a checked-in .pre-commit-config.yaml, and each developer runs one install command to wire them up. It also manages the hooks’ own dependencies in isolated environments, so a Python linter and a Go formatter and a shell checker can coexist without polluting your system.
Install the framework once, globally:
pip install pre-commit # or: brew install pre-commit
pre-commit --version2 A config that earns its keep
Here is a .pre-commit-config.yaml close to what I put in most repos. The “hygiene” hooks at the top are universal; the rest you mix to match the language:
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
args: ["--maxkb=500"]
- id: check-merge-conflict
- repo: https://github.com/gitleaks/gitleaks
rev: v8.21.2
hooks:
- id: gitleaks
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.0
hooks:
- id: ruff
args: ["--fix"]
- id: ruff-formatThen, inside the repo, each developer runs:
pre-commit install # wire it into .git/hooks
pre-commit run --all-files # check the whole repo once, nowAfter that, every git commit runs the hooks against the staged files only. If a hook modifies a file — ruff --fix reformatting, end-of-file-fixer adding a newline — the commit aborts, the file is fixed, and you re-stage and commit again. Mildly annoying the first time; deeply satisfying once it’s habit.
3 The secret scanner is the one to never skip
Every hook above is useful, but gitleaks is the one I consider non-negotiable. It scans staged changes for things that look like credentials — API keys, private keys, high-entropy strings in suspicious places — and blocks the commit when it finds one. The value isn’t the day-to-day formatting tidiness; it’s the single time it stops you committing a live token. A secret that reaches a shared branch is compromised forever, because rotating it is the only real fix once it’s in history. Catching it at the pre-commit boundary is the difference between a non-event and an incident report.
4 The honest catch: hooks are advisory
Here is the thing nobody tells you up front: pre-commit hooks run on the developer’s machine, and any developer can skip them with git commit --no-verify. They are a convenience and a safety net, not a security control. Someone in a hurry — or someone malicious — can bypass them entirely, and they only protect repos where every contributor has actually run pre-commit install.
So the correct mental model is two layers. Pre-commit hooks give you a fast local check that catches mistakes in seconds and keeps your CI green; the identical checks must also run in CI as the enforced gate that cannot be --no-verify’d away. The framework makes this easy — pre-commit run --all-files in a CI job runs exactly the same hooks as a server-side enforcement:
# in a CI workflow step
- run: pip install pre-commit && pre-commit run --all-filesRun the same config in both places and you get fast feedback locally plus an unbypassable backstop centrally. Treat the local hooks as the seatbelt and CI as the airbag.
5 Keeping the config from rotting
One maintenance note: the rev: pins in that config will drift as the underlying tools release. pre-commit autoupdate bumps them to the latest tags in one command, which I run periodically and review like any other dependency change. Pin them — don’t track moving branches — so that a hook upstream changing behaviour doesn’t silently change what your commits are checked against.
6 Is it worth it?
For any repo with more than one contributor, or any repo where a leaked secret would ruin your week, yes — this is among the cheapest high-value tooling you can add. The config is a few lines, the install is one command, and from then on the boring class of mistakes simply stops reaching your branches. New contributors get consistent formatting for free without you nagging them in review.
The honest limits: it’s advisory, so it must be mirrored in CI to actually enforce anything, and it adds a second or two to each commit, which the impatient will resent until the first time it saves them. For solo throwaway scripts it’s overkill. For everything else, install it, add gitleaks, and mirror it in CI — that combination has caught more of my mistakes than any other single tool in my workflow.




