Git Hooks: why code checks miss defects
Code review rules are almost always described: there is an agreement on style, a test suite, static analysis in the shared build pipeline. But unresolved errors, questionable style, and tests that fail at the next step still make it into the repository. The reason is the same — checks are too far from the moment when code is written.
A typical picture: the pre-commit hook is not configured, pre-push is silent, and the first signal about a problem comes from the shared build tens of minutes after the push.
By that time, the developer has switched to another task, the context is lost, and the broken build blocks the entire branch — not only the author of the last commit.
The second reason — hooks live on the machine of the person who once configured them.
A fresh clone does not get a single script until they are installed manually, so "it passes on my machine" and "it doesn't build on yours" exist at the same time, and code quality in the repository rests on discipline, not automation.
Git allows keeping git hooks directly in the repository and connecting them through shared configuration — then pre-commit checks are the same for the whole team, including new members.
We explain how this is configured and what should be checked automatically and what should be left to review.
What checks intercept at each stage of working with code
The first line of defense is pre-commit: it triggers before a change becomes part of history and works only on changed files, so it fits into seconds.
At this step, things that require no thinking are filtered out: conflict markers, debug inserts, accidentally included keys and passwords, inconsistent style, and import order.
Automatic code style checking removes a whole class of disputes: formatting ceases to be a subject of discussion in review, because it is brought to a uniform form by a tool, not by the taste of a particular engineer.
The second line of defense is pre-push, checks at the development stage before pushing a branch. Here, longer-running things are launched: build, type checking, fast tests, validation of data schemas and migrations.
A defect is cut off while the author still keeps the logic of their changes in mind — the fix takes minutes instead of sorting it out a day later, when the context is already lost.
It is precisely moving detection into the workflow that provides early defect detection without the overhead of handing a task between people.
Server-side checking remains the last line of defense and catches what bypassed local hooks: bypassing with flags, an outdated branch, merging without runs.
Unified team rules work when they are described in the repository configuration and applied equally to everyone, including new members — then review starts with logic and architecture, not with indentation and typos.
The share of nitpicks in discussions drops noticeably, and the same reviewer time goes to what truly requires a human.
The cost of fixing an error grows nonlinearly as it moves away from the moment of the change: seconds on a local run, several review iterations when found in the shared branch, rollback and analysis of consequences — if the defect made it to release.
Therefore checks should be arranged by increasing severity: instant ones before commit, heavy ones before push, the full set on the server. What works is not the number of hooks, but their coverage of the paths where defects actually appear.
Options for implementing checks: client-side, server-side, hybrid
Code checks are placed at three points: on the developer's machine, on the repository server side, and in the build pipeline.
Each catches its own class of errors and hits its own ceiling — the choice depends on what matters more: response in seconds or a rule that cannot be bypassed.
| Option | When it fits | Limitations |
|---|---|---|
| Client-side hooks (pre-commit, pre-push) | Need fast feedback on formatting and gross errors before push | Bypassed with a hook-skip flag, depend on the local environment and analyzer versions |
| Server-side checks (pre-receive, update) | Need non-disableable rules: commit message format, prohibition on editing protected branches | Trigger after push — the author sees the rejection late, fixing requires new commits |
| Checks in the build pipeline | Full run of tests, code analyzers, and build in a clean environment | Feedback comes in minutes and shares the task queue with other builds |
| Hybrid check scheme | Need both fast feedback and a barrier that cannot be bypassed | Requires synchronization of configurations and analyzer versions between levels |
Server-side checks in GitLab are connected as scripts on the repository side and trigger before changes are applied, so they do not depend on the machine settings and tool versions of the author.
In practice, the working scheme is hybrid. Client-side hooks take what should fail within seconds: formatting, fast code analyzers, commit message checking. Server-side ones take what cannot be bypassed: protected branches and naming rules.
The build pipeline retains the full test run where there are no local artifacts and developer cache.
A condition for the hybrid to work is a single analyzer configuration file for all three levels. Otherwise the local run passes and the server-side one fails on the same lines, and trust in checks disappears faster than they pay off.
How we implement Git Hooks: stages of engineering work
Hooks are integrated into an already working process rather than replacing server-side checks. We place the barrier where an error is cheapest to fix — on the developer's machine, before pushing to the shared branch.
Therefore the work goes from auditing current rules to a policy that the team then maintains itself.
- Audit of check rules. We analyze what the build pipeline already catches and which errors reach manual review. Based on this list, we select checks for the local barrier — there is no need to duplicate everything.
- Choosing the control point. Formatting, linter, and secret search in changes — at pre-commit; types and tests of affected modules — at pre-push, so the commit does not wait.
- Configuring hooks. The config lives in the repository and is installed with one command: rules arrive together with the code and are the same for everyone, including new members.
- Testing checks. We run real scenarios: broken formatting, a key in a changed file, a branch with several commits, a rename. We also look at speed — a slow hook starts getting bypassed.
- Rollout order. First warning mode, then blocking: a sharp transition produces a wave of bypasses and devalues the check.
- Handing over policies. We provide documentation: how to add or change a rule, when a bypass is justified, what to do when a hook fails on someone else's machine.
Hook configurations, check schemes, and policies for the team
We hand over not a description, but a working set of files in the repository.
Each check rule lives next to the code, is versioned with it, and triggers identically on the developer's machine and in the build environment — rules do not diverge between machines and are not lost during updates.
- Hook configurations — ready-made rules for pre-commit and pre-push: formatting, static analysis, branch name and commit message checking, a fast test set.
- Unified installer — one command after cloning connects checks for everyone. Otherwise part of the team works without them and learns about the rules only at merge.
- Check scheme — which hook launches what, what blocks a commit, what gives a warning, how long the stage takes. We move long checks to the server so the commit stays fast.
- Repository access policies — who can change the hook configuration and exception list, protection of the main branch, duplication of key checks on the server side.
- Exception instructions — how to arrange a skip for a file or rule: where the list lives, what justification is needed, who approves it, and when we review it.
- Team documentation — what to do when a check triggers, how to fix typical errors, how to update the rule set.
- Maintenance policy — frequency of updating the set, the responsible person, the procedure for adding new checks.
Acceptance takes place on your branch: we show a check triggering, a bypass according to the instructions, and passing after the fix.
Case study: how automated checks reduced defects before release
A distributed team of fourteen engineers across four time zones maintained a monolith and several services in one repository on Git. Branches lived for two to three days, and changes that touched a shared module arrived for review to a person who was asleep at that moment.
Checks caught style discrepancies and forgotten debug output only after merge — that is, when a tester, not the code author, took them on.
What we configured
The commit hook brought formatting to a common form, ran the linter on changed files, and aborted the commit if it found keys, access tokens, or environment files in the diff.
The push hook ran tests for affected modules, checked that the commit message was linked to a task in the tracker, and prohibited direct push to protected branches. Each check works only on changed paths — the run fits into tens of seconds and does not slow down work.
What changed
The share of defects found before release grew from 62% to 91%: logic discrepancies became visible on the author's machine, not on the staging environment.
Average code review time dropped from a day to five to six hours — formatting disputes left review, and one change now gets one or two substantive comments instead of six.
Returns to the author decreased threefold, because some of them were cut off even before the branch was created.
We left bypassing checks possible — only through an explicit flag, and such commits appear in a report for the manager. For emergency fixes, the team paid with predictability: violations are visible rather than dissolving in history.
How pre-commit differs from pre-push and what to choose
These are not alternatives, but two different control points with different amounts of data. pre-commit sees only files staged for commit and takes seconds — formatting, linter, key and password search, and a ban on large binary files go there.
pre-push looks at all commits of the branch being pushed, so type checking, tests of affected modules, and build are already appropriate there.
The main limitation is the developer's patience. If pre-commit runs longer than a few seconds, people start bypassing it with the --no-verify flag, and the control point becomes a formality.
pre-push triggers less often and allows tens of seconds: the cost of an error is higher, and the full picture of the branch is unavailable at the stage of a single commit.
| Parameter | pre-commit | pre-push |
|---|---|---|
| Control point | before creating a commit | before pushing a branch |
| Check scope | staged files | all commits of the branch |
| What runs | formatting, linter, secret search | types, tests, build |
| Speed | seconds | tens of seconds |
| Load on the developer | minimal, often fixes code itself | noticeable, but rare |
The choice is usually not binary: pre-commit keeps history clean and prevents garbage from accumulating, pre-push catches what requires the full picture — type discrepancies between modules, test failures at the junction of changes.
The rule is simple: the weight of a check grows along with the moment it triggers. Anything longer is moved to the server, where bypass is already impossible.
The setup practice is a shared rule set in the repository, so that the same checks work for the whole team without manual installation. If needed, we will analyze your development branch and propose a layout of checks across points.
Common technical doubts about implementing checks
Before implementing checks, engineers raise the same questions: will hooks slow down work, are they easy to bypass, how does this coexist with the build pipeline. Below are direct answers.
Will checks slow down every commit?
The check runs only on changed files from the index, not on the entire repository, and results are cached between runs. Formatting and linting on dozens of files take seconds, while heavy steps — tests, build — are moved to pre-push or the pipeline.
Will the hook still be bypassed with a flag?
Yes, a local hook is bypassed with one option, and that is normal: it is needed for fast feedback, not as a means of control.
A guarantee is provided by duplicating the same rules as a mandatory build step plus protecting branches from direct push — then bypassing the hook does not affect code quality in the branch.
How not to duplicate rules in the pipeline?
The rules live in one config inside the repository and are invoked by two profiles: a fast local one and a full one in the build. The check set is shared; depth and volume differ, so the discrepancy "passed locally, failed in the build" disappears.
We have several languages in the repository
Common steps — formatting, secret search, name checking — work across all directories. Language-specific checks are connected by path masks, and environments are isolated with containers so that dependencies of different languages do not conflict.
Who maintains the checks afterward?
The configuration lives in the repository and changes through ordinary review, like code. We hand over a policy for updating rules and an analysis of common errors so that the team can extend checks itself, without external support.
Let's discuss your task: what to prepare for the conversation
For the estimate and work plan to rely on facts rather than assumptions, a few inputs are enough for the first conversation.
There is no need to prepare a package of documents in advance — we will collect some of the data together, but the more accurate the initial picture, the shorter the path from discussing the task to starting work.
- Repositories: how many there are, whether it is a monorepo or a set of separate ones, which branches are protected from direct commits.
- Language stack and what is already checked on the continuous integration server — linters, formatting, static analysis, tests.
- Branching rules: merge order, mandatory checks before merge, who has the right to bypass them.
- Method of delivering hooks to developers' machines: a shared directory inside the repository, an initialization script, or a package manager.
- Division of checks between pre-commit and pre-push: the developer's sensitivity to waiting time versus the completeness of the rules being checked.
- Access and security policy constraints: whether a mode without external dependencies is needed, whether there are private package registries.
- Team composition and areas of responsibility: who owns code standards and who will maintain the rules after handover.
Next we agree on the target configuration, roll it out to a pilot repository, and measure check time and the share of blocked commits. Having these numbers, we fix the work plan for the remaining repositories and the order of connecting the team: the start takes days, not weeks. Describe the task and send what you have at hand — we will come back with an estimate and a plan.







