A Hook for Every Stage of the Push Lifecycle
A git push is not a single atomic event on the server. It happens in stages, and Git provides a hook at each stage. Understanding which hook fires when — and what you can still do at each point — is essential for designing the right enforcement strategy.
pre-receive fires first, before any reference (branch or tag) is updated. It receives the full list of refs being pushed and can reject the entire push in one shot. This is the most powerful hook and the one used for policy enforcement. If it exits non-zero, nothing is written to the repository.
update fires once per ref being updated. If you push two branches simultaneously, update runs twice — once for each branch. This lets you apply per-branch rules: for example, allow the push to feature/* but reject it to main. Unlike pre-receive, a non-zero exit from update only rejects that specific ref, not the entire push.
post-receive fires after all refs have been successfully updated. It cannot reject anything — the push has already been accepted. Its purpose is side effects: sending notifications, triggering CI pipelines, updating deployment systems, or writing audit logs.
git push origin main feature/login
│
▼
[pre-receive] ← Sees both refs. Can reject everything.
│
▼
[update: main] ← Per-ref. Can reject just main.
[update: feature/login] ← Per-ref. Can reject just this branch.
│
▼
[post-receive] ← Both refs accepted. Trigger CI, notify Slack, etc.
A common mistake is trying to use post-receive for enforcement. By the time it runs, the commits are already in the repository. You can send a notification, but you cannot undo the push from within the hook itself.