Server-Side Hooks: pre-receive, update, post-receive
The previous lesson's four hooks all run on your own local machine, meaning a developer could technically bypass or disable them entirely (with `--no-verify`, covered in the next lesson). Server-side hooks solve exactly this gap: running on the remote repository itself, at the moment it receives a push, they enforce policy centrally and can't be bypassed by any individual contributor's local configuration.
Learning Objectives
- Explain why server-side hooks provide stronger enforcement than client-side hooks alone.
- Understand the order and purpose of pre-receive, update, and post-receive.
- Recognize how server-side hooks relate to platform features like GitHub's branch protection rules.
- Identify realistic use cases each server-side hook is well suited for.
Key Terms to Know Before Learning About Server-Side Hooks
- Server-side hook: A hook that runs on the remote (server) repository itself when it receives a push, rather than on a developer's local machine.
- pre-receive hook: Runs once for an entire incoming push, before any of it is accepted, able to reject the whole push.
- update hook: Runs once per branch being updated within a push, able to selectively accept or reject updates to specific branches.
- post-receive hook: Runs after a push has already been fully accepted, used for triggering side effects like deployments or notifications.
How Server-Side Git Hooks Actually Work
Every hook covered in the previous lesson runs entirely on the **developer's own local machine** — which means a developer can simply skip, modify, or bypass them (for example, using `git commit --no-verify` to explicitly skip `pre-commit` and `commit-msg`, a detail worth knowing exists). This makes client-side hooks excellent for **convenience and fast feedback** (catching an issue before you even finish committing), but fundamentally **unreliable as a strict enforcement mechanism**, since compliance depends entirely on each individual developer's local setup.
**Server-side hooks** solve this gap by running on the **remote repository itself**, at the moment it receives an incoming push — somewhere no individual developer's local configuration can affect or bypass. Three key server-side hooks run, in this order, when a push arrives:
**`pre-receive`** runs first, **once for the entire incoming push** (which might contain updates to multiple branches or many commits at once), before **any** of it is accepted. If this hook exits non-zero, the **entire push is rejected** — nothing from it is accepted, even if only part of it violated some policy. This is the right place for broad, push-wide policy checks — for example, rejecting any push containing a commit whose message doesn't meet a required format, or scanning for accidentally included secrets across the whole incoming push (connecting back to Module 7's discussion of secret exposure) before anything is ever accepted onto the server.
**`update`** runs next, but **once per individual branch reference** being updated within that push (a single push might update several branches at once, particularly with `--all` or `--tags`). This gives more granular control than `pre-receive` — you could, for example, allow updates to feature branches freely while specifically rejecting any direct push to `main`, a server-side equivalent (and technically stricter, unbypassable version) of the branch protection concept from Module 4, though on GitHub itself, branch protection rules are implemented as a platform feature rather than something you'd typically write as a raw update hook yourself.
**`post-receive`** runs last, **after the entire push has already been fully accepted** — like client-side `post-commit`, it cannot reject or affect the push at this point, since it has already succeeded; it exists purely for **triggering side effects**. This is an extremely common and practically important hook in real infrastructure: triggering an automatic deployment, notifying a CI/CD system that new commits have arrived, posting a notification to a team chat channel, or updating an internal dashboard — all in direct response to code actually landing on the server.
A useful connection to draw explicitly: GitHub's branch protection rules and required status checks (Module 4 and Module 5) are, conceptually, a managed, user-friendly platform implementation of exactly this same underlying server-side hook capability — GitHub runs the equivalent enforcement logic on its own servers when you push, without requiring you to write and maintain raw hook scripts yourself. Understanding server-side hooks at this level demystifies what such platform features are actually built on top of.
Server-Side Git Hooks Timeline: Visual Walkthrough
Draw a horizontal timeline representing an incoming 'git push' arriving at a remote server, with three checkpoints: 1) 'pre-receive — runs ONCE for the entire push, before anything is accepted; non-zero exit REJECTS THE WHOLE PUSH', 2) 'update — runs ONCE PER BRANCH being updated within the push; can selectively accept/reject individual branch updates', 3) 'post-receive — runs AFTER the push is fully accepted; CANNOT reject anything, triggers side effects (deploy, notify, CI trigger)'. Add a side note: 'Unlike client-side hooks, these run on the SERVER — no individual developer's local machine can bypass them.'
Server-Side Git Hooks: Quick Reference Table
| Hook | Scope | Can Reject the Push? |
|---|---|---|
| pre-receive | Once for the entire incoming push | Yes — rejects the ENTIRE push if non-zero |
| update | Once per individual branch reference being updated | Yes — can selectively reject updates to specific branches |
| post-receive | Once, after the push has already been accepted | No — side effects only (deployment, notifications, CI triggers) |
Server-Side Hooks: Example Script Logic
# Example pre-receive hook logic (runs on the SERVER, not a developer's machine)
#!/bin/sh
# Reject the entire push if it contains a file matching a secret pattern
while read oldrev newrev refname; do
if git diff "$oldrev" "$newrev" | grep -qE 'API_KEY\s*='; then
echo "ERROR: Push rejected — possible API key detected in changes."
exit 1
fi
done
# Example update hook logic: reject direct pushes to 'main'
#!/bin/sh
refname="$1"
if [ "$refname" = "refs/heads/main" ]; then
echo "ERROR: Direct pushes to main are not allowed. Use a pull request."
exit 1
fi
# Example post-receive hook logic: trigger a deployment notification
#!/bin/sh
curl -X POST https://internal-ci.example.com/webhook/deploy-trigger
Breaking Down the Server-Side Hook Example
The `pre-receive` example scans the entire incoming push's diff for a pattern resembling an accidentally included API key, rejecting the whole push before anything is accepted onto the server if found — directly enforceable, unlike a client-side equivalent that any developer could simply bypass locally. The `update` example demonstrates branch-specific enforcement, rejecting any direct push specifically targeting `refs/heads/main`, conceptually similar to (and a raw, hand-written version of) GitHub's branch protection rules from Module 4. The `post-receive` example shows a typical side-effect trigger — notifying an external CI/CD system that new commits have arrived, exactly the kind of automatic deployment trigger this hook is commonly used for in real infrastructure.
How Server-Side Hooks Are Used to Enforce Policy Centrally
- GitHub's own branch protection rules and required status checks are effectively a managed, platform-level implementation of the same server-side enforcement concept described in this lesson, without requiring individual teams to write and maintain raw hook scripts.
- Companies running their own self-hosted Git servers (rather than relying entirely on GitHub, GitLab, or Bitbucket) frequently write custom pre-receive and update hooks to enforce organization-specific policies not available as built-in platform features.
- post-receive hooks triggering automatic deployment are a foundational building block of many continuous deployment pipelines, particularly in simpler or self-hosted infrastructure setups predating more modern, dedicated CI/CD platforms.
- Security-conscious organizations sometimes implement server-side secret-scanning via pre-receive hooks as an additional, unbypassable layer beyond client-side pre-commit checks, precisely because server-side enforcement can't be skipped by an individual developer's local setup.
Server-Side Hooks Interview Questions and Answers
Q1. Why are server-side hooks considered a more reliable enforcement mechanism than client-side hooks?
Client-side hooks run entirely on a developer's local machine and can be bypassed — for example, using git commit --no-verify to skip them, or simply never setting them up in the first place. Server-side hooks run on the remote repository itself when it receives a push, somewhere no individual developer's local configuration can affect or bypass, making them a genuinely reliable way to enforce policy centrally.
Q2. What is the difference between pre-receive and update in terms of their scope?
pre-receive runs once for the entire incoming push, and rejecting it (non-zero exit) rejects everything in that push at once. update runs once per individual branch reference being updated within the push, allowing more granular, per-branch decisions — for example, rejecting a direct push specifically to main while allowing other branches to update normally.
Q3. How do GitHub's branch protection rules relate to the concept of server-side hooks?
Conceptually, they're a managed, user-friendly platform implementation of the same underlying server-side enforcement capability. GitHub runs equivalent enforcement logic on its own servers when a push arrives, without requiring individual teams to write and maintain their own raw pre-receive or update hook scripts to achieve similar policy enforcement.
Server-Side Hooks Quiz: Test Your Understanding
1. Why can't a developer bypass a server-side hook the way they might a client-side hook?
- Server-side hooks are technically impossible to write
- Server-side hooks run on the remote repository itself, not on any individual developer's local machine
- Client-side hooks are always more powerful
- Server-side hooks require a paid GitHub plan
Answer: B. Server-side hooks run on the remote repository itself, not on any individual developer's local machine
Explanation: Since server-side hooks execute on the server receiving the push, rather than on a developer's own machine, no individual local configuration or flag can skip or disable them.
2. What happens if a pre-receive hook exits with a non-zero status?
- Only the offending commit is rejected
- The entire incoming push is rejected
- Nothing happens — pre-receive cannot reject anything
- The push succeeds but with a warning
Answer: B. The entire incoming push is rejected
Explanation: pre-receive runs once for the whole incoming push; a non-zero exit rejects everything in that push at once, rather than selectively rejecting only part of it.
3. Which server-side hook runs once per individual branch reference being updated, allowing selective, per-branch rejection?
- pre-receive
- update
- post-receive
- commit-msg
Answer: B. update
Explanation: The update hook runs once for each branch reference being updated within a push, enabling more granular decisions than pre-receive's whole-push scope, such as rejecting updates only to a specific protected branch.
Common Misunderstandings About Server-Side Hooks
- Relying solely on client-side hooks for genuinely important policy enforcement, not realizing they can be bypassed by any individual developer.
- Confusing pre-receive's whole-push scope with update's per-branch scope, misapplying one where the other's granularity was actually needed.
- Expecting a post-receive hook to be able to reject a problematic push, when by the time it runs, the push has already been fully accepted.
- Not recognizing that platform features like GitHub's branch protection rules are conceptually built on this same underlying server-side hook capability.
Server-Side Hooks: Exam-Ready Quick Notes
- Server-side hooks run on the REMOTE repository when it receives a push — cannot be bypassed by any individual developer's local machine.
- pre-receive: runs once for the ENTIRE push; non-zero exit rejects everything.
- update: runs once PER BRANCH being updated; allows granular, per-branch accept/reject decisions.
- post-receive: runs AFTER the push is accepted; cannot reject anything, side effects only (deployment, notifications).
Server-Side Hooks: Key Takeaways
- Server-side hooks provide genuinely reliable, unbypassable policy enforcement, unlike client-side hooks which depend on each developer's local setup.
- pre-receive (whole-push) and update (per-branch) offer different granularities of rejection capability during an incoming push.
- GitHub's own branch protection rules are conceptually a managed platform implementation of this same server-side hook mechanism.
Frequently Asked Questions About Server-Side Hooks
Q1. What is a server-side Git hook?
It's a hook script that runs on the remote repository itself when it receives an incoming push, rather than on a developer's local machine — meaning it enforces policy centrally in a way no individual developer's local setup can bypass.
Q2. Why are server-side hooks more reliable for enforcement than client-side hooks?
Client-side hooks run locally and can be intentionally skipped, disabled, or simply never set up by a given developer. Server-side hooks run on the remote server itself, applying to every incoming push regardless of any individual developer's local configuration.
Q3. What is the difference between pre-receive and update hooks?
pre-receive runs once for the entire incoming push and can reject it all at once. update runs once for each individual branch being updated within that push, allowing more granular, per-branch decisions about what to accept or reject.
Q4. What is a post-receive hook typically used for?
Since it runs only after a push has already been fully accepted, it can't reject anything — it's typically used for side effects like triggering an automatic deployment, notifying a CI/CD system, or posting a notification to a team chat channel.
Q5. How do GitHub's branch protection rules relate to server-side hooks?
They're conceptually a managed, platform-level implementation of the same underlying server-side enforcement capability — GitHub handles this logic on its own servers, so individual teams don't need to write and maintain raw pre-receive or update hook scripts to achieve similar policy enforcement.
Summary
Server-side hooks run on the remote repository itself at the moment it receives a push, providing genuinely reliable policy enforcement that no individual developer's local machine can bypass — unlike the client-side hooks from the previous lesson, which can be skipped (for example, with `--no-verify`). `pre-receive` runs first, once for the entire incoming push, and can reject everything in that push at once with a non-zero exit. `update` runs next, once per individual branch reference being updated within the push, enabling more granular, per-branch accept/reject decisions — such as rejecting a direct push specifically to `main`. `post-receive` runs last, only after the push has already been fully accepted, purely for triggering side effects like deployments, CI/CD notifications, or team chat alerts, since it's too late by that point to reject anything. GitHub's own branch protection rules and required status checks are conceptually a managed, user-friendly platform implementation of this exact same underlying server-side enforcement capability.