skip to content

In Git, what runs in the post-receive hook and why can it not reject a push?

level: middleimportance: should knowfreq 32%

answer

  1. Same stdin format as the earlier whole-push hook
  2. Ordering, not permission, is the reason
  3. The refs have already moved
  4. Its output does reach the pusher
  5. Do not hold the push open

basics

~20 s

post-receive runs once on the receiving repository after every ref has already been updated, reading the same old/new/ref lines on stdin. The work is done by then, so its exit status cannot refuse anything — it exists for notifications and follow-on actions.

solid answer

~50 s

`post-receive` is invoked by `git receive-pack` once per push, **after** the refs have actually been updated. It takes no arguments and gets the same standard-input lines as `pre-receive` — `<old-oid> <new-oid> <ref-name>`, one per ref — so it knows exactly what moved and from where. Git documents that it does not affect the outcome of `receive-pack`, because it is called after the real work is done; exiting non-zero changes nothing about the refs. Its useful property is that both its stdout and stderr are relayed to the person pushing, so it can print a build URL or a warning into their terminal. Typical uses are notification, triggering a downstream job, or updating a mirror. The related `post-update` hook also runs after the fact but receives the updated ref *names* as arguments, and is classically used to run `git update-server-info` for dumb-HTTP serving.

code

bash · 11 lines
bash
#!/bin/sh
# hooks/post-receive — no arguments; same stdin lines as pre-receive
z40=0000000000000000000000000000000000000000
while read -r old new ref; do
  [ "$ref" = "refs/heads/main" ] || continue
  [ "$new" = "$z40" ] && continue          # main was deleted
  count=$(git rev-list --count "$old..$new" 2>/dev/null || echo '?')
  echo "received $count new commit(s) on main"   # relayed to the pusher
  # hand off quickly; do not run the deploy inline
done
exit 0

go deeper

for a junior

Recall the simple rule: hooks that run after the refs are updated can only report, never refuse. Knowing post-receive is where notifications go is enough here.

for a middle

Explain the interface — no arguments, the same stdin lines as pre-receive — and why the ordering, not any permission model, is what makes rejection impossible.

for a senior

Show operational judgment: filter to the refs you care about, hand off long work instead of blocking the push, handle deletions, and never try to undo a push after the fact.

for a principal

Own where push-triggered automation belongs at all: coupling deployments to a hook on the receive path concentrates risk and latency in the one place every contributor must pass through.

## Position in the sequence When a push arrives, the receiving repository runs, in order: `pre-receive` once for the whole push, `update` once per ref, then the ref updates themselves, then `post-receive` once, then `post-update`. Everything before the ref updates can refuse; everything after can only react. `post-receive` is the first hook on the far side of that line. ## Interface `post-receive` takes no arguments and reads standard input in exactly the same format `pre-receive` uses: ``` <old-oid> SP <new-oid> SP <ref-name> LF ``` One line per ref that was updated. An all-zeroes old value means the ref was created; an all-zeroes new value means it was deleted. Since it runs after the fact, those values describe what *did* happen rather than what was proposed, and every line corresponds to a ref that survived both earlier gates. ## Why it cannot reject The reason is purely one of ordering, not of permission: the refs have already moved. Git states that the hook does not affect the outcome of `receive-pack` because it is called after the real work is done. A non-zero exit does not roll anything back. A hook that tried to "undo" a bad push by resetting the ref itself would be racing every other client and destroying history behind the pusher's back — the rejection belongs in `pre-receive` or `update`, where it is cheap and honest. ## What it is actually for The hook's value comes from two properties: it knows precisely which refs changed, and its output reaches the pusher's terminal (unlike the `update` hook, whose stdout is discarded, both streams of `post-receive` are forwarded to the client). That combination supports: - **Notification** — announcing the new commits to a chat system or mailing list. - **Triggering follow-on work** — kicking off a build or a deployment for a specific branch, and printing where to watch it. - **Mirroring** — pushing the same refs on to a secondary repository. - **Housekeeping** — refreshing derived data that depends on the refs. A branch filter is almost always the first thing in the script: react to `refs/heads/main`, ignore everybody's feature branches and tags. ## Keep it off the critical path The pusher's `git push` does not return until `post-receive` finishes. A hook that runs a deployment synchronously turns every push into a multi-minute wait, and the pusher's network hiccup can kill the job halfway. The standard discipline is to do the smallest possible thing in the hook — enqueue, notify, or hand off — and let the long-running work happen elsewhere. It is also worth making the hook resilient: it runs after the data is safely stored, so failing loudly is fine, but hanging on an unreachable network endpoint holds the developer's terminal hostage. ## post-update, briefly `post-update` also runs after the refs are updated, but its interface is different: it receives the names of the refs that were actually updated as command-line arguments, with no object names and no stdin. Its canonical use is running `git update-server-info` so that a repository served over dumb HTTP keeps its auxiliary files current. When you need old and new object names, `post-receive` is the hook you want. ## The interview point The examiner is checking that you have internalised a two-line rule: hooks *before* the update decide, hooks *after* the update inform. Candidates who suggest "reject bad pushes in `post-receive` and reset the branch" have missed that Git already gave them two hooks that run before anything moves.

  • How does post-receive differ from post-update?
    `post-receive` runs once, takes no arguments and reads old object name, new object name and ref name from stdin, so it knows exactly what changed. `post-update` receives only the names of the updated refs as arguments, with no object names. Use `post-receive` for anything that needs a commit range; `post-update` is classically just `git update-server-info` for dumb-HTTP serving.
  • Why is running a deployment synchronously in post-receive a bad idea?
    Because the developer's `git push` does not return until the hook exits. A multi-minute deploy blocks their terminal, and a dropped connection or an impatient Ctrl-C can leave the job half-done. Enqueue the work and return immediately — the hook's job is to signal that refs changed, not to perform the whole downstream pipeline.
  • Can post-receive tell that a branch was deleted?
    Yes. The line for that ref carries an all-zeroes new object name, exactly as in `pre-receive`. Hooks should test for it before computing a range, since `git rev-list <old>..<zeroes>` is meaningless. A creation shows the mirror image: an all-zeroes old object name.

saying these in an interview costs you the question

  • Proposing to reject a bad push from post-receive
  • Resetting the ref in a post-hook to undo a push
  • Expecting ref names as arguments rather than stdin lines
  • Running a full deployment inline and blocking the push
  • Assuming post-receive output is hidden from the pusher

context