Husky Git hooks: where pre-commit check breaks
Hooks are committed, the linter passes locally — but for part of the team, the commit goes through without a single check.
Typical causes: the hooks directory is not set via path configuration, the file lost executable permission when transferred between file systems, dependency installation in the build runs with build scripts disabled.
Husky makes git hooks in a project part of the repository rather than a developer's personal setup — but only with reproducible installation.
The cost of commit failures does not show up immediately. Style diverges between branches, and review turns into an argument about formatting: real edits get lost among reformatted lines.
Type and syntax errors surface at merge, when the author has switched to another task, the build is red, and the release slips.
The second half of failures is the hook configuration itself. The pre-commit check runs across the entire tree instead of staged files: on an average project this is tens of seconds, and bypass via --no-verify becomes a habit.
With partial staging (git add -p), the linter reads the working copy, while the commit gets the version from the index — unchecked. In a repository with multiple packages, the common hook lints everything entirely.
According to industry estimates of the development process, a defect caught before build costs several times less in labor than the same defect at integration.
We break down how this is addressed: reproducible hook installation, checking exactly the lines that go into the commit, and response within a couple of seconds.
What automatic code checks before push provide
Feedback in seconds instead of waiting for review
The pre-commit linter and type checking run locally in seconds — the developer learns of an error before the code goes to the shared branch. Previously, the same error surfaced at build or review after several hours, and time was spent on context switching. Fast feedback reduces the correction cycle from hours to minutes.
Unified style and fewer edits when merging branches
Automated code checks lock in formatting, import order, and naming rules before merge, so controversial edits in review become a thing of the past. The share of style-related comments falls almost to zero — discussion shifts to logic and architecture.
A unified code style also removes some merge conflicts: formatting differences stop producing false differences in files.
Build stops failing over trifles
Checks cut off what used to break the build: unclosed types, forgotten debug output, unused dependencies. The share of build failures drops many times, and freed time goes to tasks, not sorting out someone else's typos.
The volume of repeated fixes in the same file also falls — the rule triggers once and works for the whole team.
Hooks that Husky runs are configured for a specific repository: heavy directories are excluded from checks, and an explicit bypass remains for urgent fixes. Rules live in the project itself, so they work the same for everyone — including a new developer from day one.
Ways to connect checks: locally, in build, in repository
Checks can be enabled at three levels: on the developer machine, in the build pipeline, and in the shared repository. Levels do not replace each other — they catch different classes of violations: from a typo in a commit to a secret getting into the shared branch. Below is what each option provides and when it is sufficient.
| Option | When suitable | Limitations |
|---|---|---|
| Local hooks on workstation | Team of 2-10 people, single stack, dependencies installed from manifest on clone | Bypass via --no-verify, fresh clone without dependency installation, different environments among participants |
| Build-side checks | Multiple teams, long branches, mandatory checks before merge | Feedback arrives later — minutes, not seconds; as the number of branches grows, a task queue accumulates |
| Control in shared repository | Unified rules for everyone, protection of release branches and tags | Requires permissions to configure, some checks duplicate local ones, rules are harder to change selectively |
When local control is enough
As long as rules concern format, linter, and commit messages, local hooks are enough — they provide feedback in seconds, before code goes to the shared branch.
It is enough for dependencies to be installed during the package installation step, not by a separate command that a newcomer will forget to run.
When build-side control is needed
As soon as a repository has multiple teams or release branches, local control stops being a guarantee: no one has revoked the bypass flag.
Then the same checks are duplicated in build and become a mandatory condition for merge — a violation does not pass further, even if it was bypassed on the machine. Local hooks remain: they save time, while build-side ones protect the shared branch.
Implementing Git hooks: stages of engineering work on a project
Implementing git hooks starts not with installing Husky, but with a repository audit: we look at which checks already live on the build server, how many branches live long, where declared rules diverge from the actual code.
Based on the results, we assemble a work plan in which each hook is tied to a specific class of errors, not to an abstract "need to check everything." Below are the setup stages from audit to handover to the team.
What we do at each step
- Repository audit: inventory of linters, formatters, and tests, finding duplicates with continuous integration, measuring run times.
- Hook scheme: commit-msg is responsible for message format, pre-commit — for files in the index, pre-push — for types and fast tests.
- Setting up the team environment: dependencies and prepare script in the manifest — hooks install during installation, without manual steps on each machine.
- Connecting style and type checks: shared configs so the editor, hooks, and build server read one source of rules.
- Debugging on real branches: long branch, mass rename, merge — we look at check time and friction with frequent commits.
- Checking bypass behavior: what happens with
--no-verify, what gets into history in that case, and how the build server picks it up. We do not prohibit bypass, but make it noticeable. - Handover and instruction: guide for manual run, updating configs, and handling triggers, a short session with the team.
Alignment points with the team
We agree on the set of hooks and the strictness threshold: what blocks a commit and what gives a warning. Separately, we record who owns the configs and how to make changes — otherwise rules diverge between machines and the build server in a couple of weeks.
Deliverable package: configurations, schemes, regulations, documentation
We assemble the package so that a new engineer can get involved without verbal explanations, and rules live in the repository, not in chat. Below is what exactly we hand over to the customer along with configured Git hooks.
Technical documentation
- Check configuration files: hook settings and related code analyzers in the repository, with comments for each rule and markup of what is checked at commit and what before sending to the shared branch.
- Scheme of a commit passing through stages: from edit in the working copy to merge — which checks run, what happens on error, and what a correct pass without bypass looks like.
- Project documentation map: where style rules, check configuration are located, and how they relate to the engineer's local settings.
Regulations and instructions for the team
- Development regulations: branch and message naming rules, title and body format, requirements for splitting changes into atomic commits.
- Check bypass policy: when an emergency skip is permissible, who confirms it, what remains in history, and how the incident is reviewed afterwards.
- Guide for new developers: step-by-step environment setup, first commit, common errors and ways to fix them.
- Rule update regulations: who makes changes to the check configuration, how approval proceeds, and how to ensure new rules do not block the team's work.
Case: pre-commit check instead of failures in the main branch
Before and after in numbers
Before implementing checks on the developer side, the main branch built as luck would have it.
According to build server logs: 34% of runs failed at the linter and test stage — almost every third; first feedback arrived on average after 11 minutes, and on formatting edits waiting stretched to half an hour. Returns of a task to work after review were just as many — edits arrived after the server run.
After moving basic checks into the local loop via Husky, the share of build failures in the main branch fell to 6%, first feedback comes in 30-40 seconds on the same set of checks, and returns to work decreased to one task out of twelve.
Checks run on changed files, not the whole repository, so waiting remains short even on a large codebase.
What we did and what changed
We configured the pre-commit hook for formatting and linter with autofix, and moved unit tests for affected modules and type checking to pre-push. Heavy scenarios were left on the build server so the local loop does not turn into waiting.
Hook configuration lies in the repository and rolls out together with dependency installation — a new engineer gets the same rules on the first day, without verbal agreements.
The bypass path is also closed: shared hooks on the server side ignore local relaxations, and any configuration change goes through review like regular code.
If necessary, the hook is removed with one command, edit history is preserved — rollback to the previous mode does not require rebuilding the project.
What to do if checks are already partially configured in a project?
Partially configured hooks are the norm, not the exception: usually a project already has Husky with a linter at the pre-commit stage, plus a couple of local scripts and a duplicate check on the server pipeline.
We start with inventory: write down each rule, its run stage, execution time, and affected files. Without such a map, any config change turns into guessing why one engineer's commit passes and another's fails.
Next we sort rules into groups. Fast ones — formatting, linter, type checking on changed files — remain at the commit stage. Heavy ones — build, tests, dependency checks — move to the pre-push stage or server.
We remove duplicates in favor of a single source of truth: a rule living in three places diverges by versions at the first edit.
Transition without stopping work is built on warning mode: a new rule first prints a notice but does not block the commit, and becomes blocking after an observation period when triggers become few.
This way the team does not go into bypass via skipping checks and does not lose its familiar rhythm.
Check migration requires continuity: we delete local scripts only after their behavior is reproduced in the shared config, and we format changes as one commit that can be rolled back entirely. Exceptions for individual paths we keep in exclusion files, not in the hook body.
We run commands through a shared wrapper — then behavior is the same on all platforms and predictable in time.
Technical doubts before implementing checks in a project
Before enabling hooks in the shared process, teams have the same doubts: will they bypass the check, will the commit start taking minutes, how does this coexist with the shared repository. Below are short answers to the most frequently asked questions.
Can the check be bypassed and how to track it?
The --no-verify flag disables hooks for one commit, and it cannot be prohibited on the developer side. Therefore critical rules are duplicated by a check on the build server: locally they provide fast feedback, and at integration they work as a mandatory barrier. Commits that passed without a check are visible in history — this is discussable, not anonymous.
Will this slow down work on a large project?
What slows things down is not the hook but a naive run of all rules across the entire tree. Checks run only on changed files with result cache, so on a project with tens of thousands of files the commit remains within seconds. We move heavy runs to pre-push or the server.
We have a shared repository, everyone has their own settings
We keep configuration in the repository, not in local settings: after cloning, the prepare script picks it up, and a new participant gets the rules immediately. We move personal experiments to a separate path via core.hooksPath so they do not diverge from the shared set.
Who maintains rules when code evolves?
Rules lie next to the code and are updated by the same change as the code: architecture changes — checks change too. Once a quarter we do a review and remove rules that stopped catching errors, otherwise hooks only slow down commits.
Let's discuss the task and come back with an estimate and work plan
To make the estimate accurate, not "plus or minus," the conversation should start with project inputs.
Setting up Husky and hooks on pre-commit and pre-push is a task where the result depends on details: how many developers are in the team, which checks already exist, and how long a full build takes on the developer machine.
What is useful to send in advance:
- repository structure: monorepo or a set of related packages;
- current checks — linter, types, tests, formatting — and which of them run manually;
- team composition and work mode: how many people and how often commits happen;
- process pain points: slow checks, merge conflicts, rule bypass via the skip flag;
- security requirements: closed environment, prohibition on sending code to external services;
- desired timelines and milestones to reach;
- procedure for repository access and project handover, if we work on your side.
Based on these inputs, we come back with an estimate and work plan: what we do first, what can be postponed, how we verify the result at each step.
Describe the task in a convenient format — we will come back with an estimate, plan, and procedure for handing the project over to your team.







