Learning Objectives
By the end of this lesson you will be able to:
- Share hooks with a team using
core.hooksPath - Describe what a hook manager such as Husky or pre-commit adds
- Place each check at the right layer: hook, CI, or branch protection
- Propose a hook to a team in a way that gets adopted
The Problem
You have written excellent hooks. They are in .git/hooks/, which is not part of the repository, so nobody else has them. Cloning the project gives a colleague the sample hooks and nothing more.
There are three ways out, and they differ in how much they ask of the team.
Option 1: core.hooksPath
Git can look for hooks somewhere other than .git/hooks/, and that somewhere can be a committed directory.
mkdir .githooks
git mv .git/hooks/pre-commit .githooks/pre-commit # or just write them there
chmod +x .githooks/*
git add .githooks
git commit -m "chore: add shared git hooks"
Each person then enables it once:
git config core.hooksPath .githooks
Advantages: no dependencies, works with any language, the hooks are reviewed like any other code.
The catch: that one config command is per-clone and manual. Somebody will forget, and there is no signal when they do. Put it in the README setup steps and in your project's bootstrap script:
{
"scripts": {
"prepare": "git config core.hooksPath .githooks"
}
}
npm runs prepare automatically after npm install, which closes the gap for a JavaScript project.
Option 2: A Hook Manager
For a JavaScript project, Husky:
npm install --save-dev husky
npx husky init
That creates .husky/, commits it, and wires up the prepare script so every npm install installs the hooks. Adding one:
echo "npm test" > .husky/pre-push
For anything else, the pre-commit framework (Python, but language-agnostic):
# .pre-commit-config.yaml
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: trailing-whitespace
- id: check-merge-conflict
- id: detect-private-key
- id: check-added-large-files
pre-commit install
The value here is not the plumbing — it is the catalogue. detect-private-key, check-merge-conflict and check-added-large-files are exactly the checks you would eventually write by hand, already written and maintained by people who have seen every edge case.
The catch: a dependency, a version to keep current, and a config file to understand. Worth it for a team; overkill for a small repository where core.hooksPath will do.
Option 3: Server-Side Enforcement
The only option that cannot be skipped, because it does not run on the developer's machine.
Branch protection rules (GitHub, GitLab, Bitbucket): require a Pull Request, require approvals, require status checks to pass, block force-pushes. Configured once by an admin and applied to everybody.
Server-side hooks (pre-receive) on a self-hosted server: the same idea, arbitrary code.
CI pipelines: run the full suite on a clean machine, and mark the result as a required check.
This is what "the real gate" means. --no-verify reaches nothing here.
Where Each Check Belongs
| Layer | Speed | Skippable | Put here |
|---|---|---|---|
pre-commit |
Seconds | Yes | Formatting, lint, secrets, debug leftovers |
pre-push |
Under a minute | Yes | Fast unit tests, no-push-to-main |
| CI | Minutes | No | Full suite, integration, e2e, coverage, security scan |
| Branch protection | Instant | No | Required reviews, required checks, no force-push |
The layers are not alternatives. A well-set-up project has all four, arranged so the cheap fast ones catch a problem first and the slow authoritative ones exist to be right.
The design principle: each layer catches what the one above it might have missed, and only the bottom two are guaranteed to run. Design the hooks for speed and convenience; design CI for correctness.
Proposing This to a Team
Three things make the difference between adoption and a rejected pull request.
Start with one hook that has no false positives. A no-push-to-main hook, or a secret scanner. Something nobody can argue with and nothing legitimate trips. Trust is built one hook at a time.
Make it opt-in first. Commit the hooks, document the one-line enable command, let people turn it on. When two people say it saved them, propose making it automatic.
Never make it slow. One colleague waiting sixty seconds to commit will disable it and tell everybody else to do the same. Speed is not a nice-to-have here; it is the adoption strategy.
And frame it honestly: hooks are a convenience that saves everyone time. They are not a control, and presenting them as one is how they get resisted.
Closing the Trail
You started this trail able to branch, merge and resolve a conflict. You can now rewrite history deliberately, move a single fix between branches, undo anything at the right depth, recover work everybody else would consider lost, find the commit that caused a regression in nine tests, and automate the checks that stop the whole cycle starting again.
That is the full set of Git operations a senior QA engineer uses. There is no Trail 4 waiting with the real secrets — from here it is practice, in a scratch repository first and a real one after.
Pro Tip: Pick one thing from this trail and use it this week.
git bisect runon a real regression is the one with the largest immediate payoff, and it is the one that will get noticed.
Key Takeaways
.git/hooks/is never committed, so hooks need deliberate sharingcore.hooksPathpoints Git at a committed directory — simple, but enabled per clone- Husky (JavaScript) and pre-commit (any language) automate installation and provide a check catalogue
- Branch protection and CI are the only layers that cannot be skipped
- Layer the checks: fast and skippable near the developer, slow and authoritative on the server
- Adoption comes from one uncontroversial hook, opt-in first, and never being slow
- Hooks are a convenience for the team, not a control over it