Git Internals
Topic 25

Hooks

Internals

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 vs Server-Side Hooks

Client-sidepre-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-sidepre-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.

Common Mistakes
  • Relying on a pre-commit hook to enforce a security rule, when any developer skips it with git 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-commit hook 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-receive hook where it cannot be bypassed.
Best Practices
  • Enforce non-negotiable policy in a server-side pre-receive or update hook, never only client-side.
  • Distribute team hooks by pointing git config core.hooksPath .githooks at a tracked directory.
  • Keep pre-commit hooks under a second by deferring heavy checks to pre-push or CI.
  • Validate commit messages in a commit-msg hook to enforce conventional-commit format.
  • Manage client hooks with the pre-commit framework rather than hand-copying scripts into each clone.
Comparable toolsMercurial extensive hooks in hgrc including server-side enforcementSubversion server-side hooks only, no client hooksPerforce triggers for server-side enforcement

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 commit writes 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.hooksPath at 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/hooks directory 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