Which checks belong in Git hooks versus CI, and why can hooks never be the enforcement boundary?
answer
- Advisory versus mandatory
- Who decides whether it runs at all
- A flag exists to skip it
- Enforcement must sit where the author cannot reach
basics
~20 sLocal Git hooks are opt-in, run on the developer's machine, and can be skipped, so they are fast feedback rather than a guarantee. Keep them sub-second and mechanical; put every check whose result you actually rely on where the developer cannot bypass it.
solid answer
~50 sLocal hooks are advisory by construction. They only exist in a clone where someone ran the installer, they run in that developer's environment with that developer's tool versions, and `git commit` and `git push` both accept a flag that skips them entirely. Anything you would block a release on therefore cannot live there. My split is: hooks get mechanical, file-local, sub-second work — formatting, trailing whitespace, commit-message shape, a fast lint on staged files — chosen because catching those before the commit is cheaper than catching them in review. Everything slow or global — the test suite, cross-module type checking, dependency and license audits, build reproducibility — runs on the pushed commits, where the check is uniform and unskippable. The Git-level way to make a rule genuinely mandatory is a server-side hook on the receiving repository, not a client-side one.
go deeper
Know that hooks run on your own machine, can be skipped, and are there to catch small things early. Whatever really matters is checked again after you push.
Explain the three reasons a local hook is advisory — opt-in installation, an available skip flag, and an uncontrolled environment — and give concrete examples of what fits a sub-second budget versus what does not.
Show you have tuned this in practice: measuring hook latency, noticing when a team starts bypassing habitually, and moving work to where it belongs. Be able to name the Git-level mechanism that makes a rule genuinely mandatory.
Own the policy design: which rules are advisory and which are gates, how the same rule is expressed in both places without drifting, and how you keep local feedback fast enough that people never learn the bypass habit.
## Why a local hook cannot be a guarantee Three independent reasons, any one of which is fatal to using hooks as enforcement: **They are opt-in.** Hook scripts can be versioned, but *enabling* them is a local action — setting `core.hooksPath`, or running a manager's install command — because Git configuration is not transferred by a clone. That is deliberate: if a repository could enable its own hooks, cloning an untrusted repository would execute its code. So the honest model is that some fraction of clones has no hooks at all, and nothing tells you which. **They are bypassable.** Both `git commit` and `git push` accept `--no-verify`, which skips the corresponding hooks. This is a legitimate feature — you need it when the hook itself is broken, or during an emergency fix — but it means the hook is a reminder, not a gate. **They run in an uncontrolled environment.** The developer's OS, tool versions, locale, and PATH decide the result. A formatter one minor version apart produces different output; a check that passes on one machine fails on another. Reproducibility is exactly what an enforcement point needs and exactly what a laptop does not offer. ## The time budget A `pre-commit` hook runs in the foreground on every commit. The practical budget is a few hundred milliseconds, and certainly under a second. `pre-push` can afford more — it runs far less often and the developer is already waiting on the network — but even there, a minute is enough to make people push with the skip flag. The budget is not a nice-to-have. A hook that exceeds it gets bypassed habitually, and a habitually bypassed hook is negative value: it costs time, teaches people to reach for the skip flag reflexively, and provides nothing. Keeping hooks fast is what preserves their usefulness, which is why staged-file scoping exists. ## What actually belongs where **Good candidates for `pre-commit`:** formatting and import ordering on staged files; trailing whitespace and end-of-file fixes; a fast syntax-level lint on the staged paths; a scan for obviously committed secrets or large binaries. Common thread: fast, file-local, mechanical, and either auto-fixable or a clear yes/no. **Good candidates for `commit-msg`:** message shape — a conventional-commit prefix, an issue reference, a subject-length limit. Cheap, and catching it at write time is far better than asking someone to rewrite history later. **Good candidates for `pre-push`:** a quick targeted test subset, or a guard against pushing to a protected branch name. Optional, and worth measuring before adding. **Belongs in CI instead:** the full test suite; cross-module or whole-program type checking; dead-code and unused-dependency analysis; dependency vulnerability and license audits; container or artifact builds; anything that needs services, credentials, or more than a few seconds. These are global by nature — restricting them to staged files gives a meaningless answer — and they need a controlled environment to be trustworthy. ## The Git-level way to make something mandatory If a rule must hold for every commit that lands, it has to be checked somewhere the author does not control. At the Git level that means the receiving repository: a hook on the server side runs on the machine accepting the push, sees the refs and objects being proposed, and can reject the whole push. That is a genuine gate because there is no client flag that skips it. The design consequence is that a client-side hook and a server-side or pipeline check are *the same rule expressed twice*, for different purposes: locally for speed of feedback, centrally for the guarantee. Duplication is the point, not a smell — but keep the definition in one place so the two cannot drift. ## Judgment questions worth raising - **Does this check earn its milliseconds?** Measure the hook. If commit latency rises noticeably, something has to move to CI. - **What is the failure mode when it is skipped?** If the answer is "a formatting nit in review", a hook is fine. If it is "a vulnerable dependency ships", it never belonged in a hook. - **Does the local check match the central one?** A hook that passes while CI fails on the same commit is worse than no hook, because it destroys trust in both. - **Is the local check auto-fixing or merely complaining?** Blocking a commit to tell someone about something a machine could have fixed is a poor trade.
- If hooks are bypassable, why run any check locally at all?Because feedback latency dominates developer cost. Catching a formatting slip in 200 milliseconds beats discovering it after a full pipeline run, and it keeps trivia out of code review entirely. The hook is an ergonomics tool: it makes the common case pleasant. It is not the guarantee, and treating it as one is the actual mistake.
- How do you keep the local hook and the central check from drifting apart?Define the rule once — the same formatter version and configuration, the same lint config, invoked by the same script — and have both the hook and the pipeline call it, differing only in scope: staged paths locally, the whole tree centrally. Pin the tool version in the project rather than relying on whatever the developer has installed, or the two will disagree on identical code.
- What is the Git-level mechanism for a rule that genuinely cannot be skipped?A hook that runs on the repository receiving the push. It executes on the server rather than the client, sees the proposed ref updates and objects before they are accepted, and rejects the entire push by exiting non-zero. Since no client flag reaches it, it is a real gate — unlike anything installed in the developer's clone.
- How would you decide whether a slow check may stay in pre-push?Measure how often people push, multiply by the added latency, and compare that to how often the check would actually have caught something before CI did. If the check rarely fires, or the pipeline finds it minutes later anyway with no cost to anyone, it belongs in CI. The moment developers start pushing with the skip flag by reflex, the answer is already no.
saying these in an interview costs you the question
- Treats a pre-commit hook as a security or policy guarantee
- Puts the whole test suite in a pre-commit hook
- Assumes every clone has the hooks installed
- Says hooks make CI checks redundant
- Blocks commits on problems a formatter could have fixed automatically