Hooks
Hooks are executable scripts Git runs automatically at defined points in its lifecycle. Client-side hooks like pre-commit run on the developer's machine and give fast local feedback; server-side hooks like pre-receive run on the repository everyone pushes to, which is the only place policy can actually be enforced. The distinction between those two locations is the whole story of when a hook is convenience and when it is a control.
The recurring lesson with hooks is that anything client-side is advisory — a developer can skip it — and only the server gets the final say on what enters the shared repository.
Where Hooks Live and How They Fire
Hooks are scripts in .git/hooks/, or in whatever directory core.hooksPath points at. Each is named for the event that triggers it — pre-commit, commit-msg, pre-push, and so on — and must be marked executable to run at all. Git invokes the matching script at the right moment, passing arguments and sometimes data on stdin according to the hook type. A non-zero exit from a "pre" hook aborts the operation.
A common silent failure is a hook file that is present but not executable: Git simply never runs it, and nothing warns you that your check is doing nothing.
Client-Side Hooks
pre-commit, commit-msg, prepare-commit-msg, and pre-push run locally during the corresponding operation. They are excellent for fast feedback — formatting, linting, commit-message shape — but they share two limits: any developer can bypass them with git commit --no-verify, and they never travel with a clone, so a teammate who clones your repo does not get them.
Because they are bypassable and local, client-side hooks are the wrong place for any rule that must hold. They make the right thing easy, not mandatory.
Server-Side Hooks
pre-receive, update, and post-receive run on the remote when it receives a push. The pusher cannot bypass them, which is what makes them the real enforcement point: a pre-receive hook can reject a push that violates branch protection, message conventions, or any other non-negotiable rule. If a policy genuinely matters, it lives here, not in a pre-commit script.
Distribution and Frameworks
Since hooks are not cloned, teams need a way to share them. Pointing core.hooksPath at a tracked directory like .githooks puts the scripts under version control and makes everyone use the same set once they configure it. Beyond that, frameworks like the pre-commit tool and Husky manage installation and updates so you are not hand-copying scripts into every clone.
$ git config core.hooksPath .githooks $ ls -l .githooks/ -rwxr-xr-x pre-commit -rwxr-xr-x commit-msg $ git commit --no-verify # skips every client-side hook
Client-side — pre-commit, commit-msg, pre-push run on the developer's machine for fast feedback, but any developer can skip them with --no-verify and they never travel with a clone. Use them for convenience, never for enforcement.
Server-side — pre-receive and update run on the central repo when a push arrives and cannot be bypassed by the pusher. This is the only place a security or branch-protection rule can actually be enforced.
- Relying on a
pre-commithook to enforce a security rule, when any developer skips it withgit commit --no-verify. - Assuming a teammate's clone runs your local checks, forgetting that hooks are not cloned and must be distributed deliberately.
- Writing a
pre-commithook that runs the full test suite, making commits so slow that everyone disables it. - Leaving a hook file non-executable, so Git silently never runs it and the check appears to pass.
- Putting branch-protection logic only in a client-side hook instead of a server-side
pre-receivehook where it cannot be bypassed.
- Enforce non-negotiable policy in a server-side
pre-receiveorupdatehook, never only client-side. - Distribute team hooks by pointing
git config core.hooksPath .githooksat a tracked directory. - Keep
pre-commithooks under a second by deferring heavy checks topre-pushor CI. - Validate commit messages in a
commit-msghook to enforce conventional-commit format. - Manage client hooks with the pre-commit framework rather than hand-copying scripts into each clone.
Knowledge Check
Why can't a client-side pre-commit hook enforce a security rule?
- A developer can bypass it with
git commit --no-verify, and it never travels with a clone - It runs far too late, only after the commit has already been created and pushed to the upstream remote
- It cannot read the contents of files already staged in the index
- Git disables client hooks once a branch is marked protected
Where does a pre-receive hook run?
- On the remote repository when it receives a push, where the pusher cannot bypass it
- On the developer's own local machine, fired just before
git commitwrites the new commit object - In a CI runner, as a separate job kicked off after the push completes
- On every fresh clone, the first time the repository is fetched
Why don't hooks travel with a clone, and how do teams cope?
- Hooks sit outside the tree Git transfers, so teams point
core.hooksPathat a tracked directory - Hooks are encrypted at rest and only the original author holds the key to decrypt them
- Git strips hooks from the transfer for security, so teams email the scripts around instead
- They do travel with the clone; the
.git/hooksdirectory is just usually empty
A hook script exists but never runs. What is the likely cause?
- The file is not marked executable, so Git silently skips it
- The script is too short for Git to recognize it as a valid hook
- Git only runs hooks while you are on the default branch
- The hook must be committed to history before it can fire
You got correct