Setting up Git Hooks (pre-commit, pre-push) for code checks

Defects and style discrepancies are easier to prevent than to look for during review and release. We set up Git Hooks turnkey: checks run automatically at every stage of work with code, before the change enters the shared repository. The team gets uniform rules and significantly fewer returns for rework.

Our competencies:

Frequently Asked Questions

Latest works

  • Development of a web application for FEEDME
    Development of a web application for FEEDME
    1365
  • Development of an online store for the company FURNORO
    Development of an online store for the company FURNORO
    1330
  • B2B Advance company logo design
    B2B Advance company logo design
    776
  • Development of a web application for Enviok
    Development of a web application for Enviok
    1077
  • AIDER company logo development
    AIDER company logo development
    1022
  • CRM development for Chasseurs
    CRM development for Chasseurs
    1113

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.

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. Rollout order. First warning mode, then blocking: a sharp transition produces a wave of bypasses and devalues the check.
  6. 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.