Chapter 8.8 — Server-Side Hooks☕ 16 min read

Server-Side Hooks

Client-side hooks? --no-verify se bypass. Server-side hooks? BYPASS NAHI HOGA. Yeh asli enforcement hai.

01Server-Side Hooks: The Unbreakable Laws

Client-side hooks are guidelines. --no-verify bypasses them. Developers can skip pre-commit, commit-msg, prepare-commit-msg — all of them. One flag and your enforcement is gone.

Server-side hooks are LAWS. They run on the Git server, not on the developer's machine. When a developer pushes, the server runs its hooks BEFORE accepting the push. If a server hook exits non-zero, the push is REJECTED — the data never enters the remote repository.

There is no --no-verify for server hooks. That flag only skips local hooks. The server does not care what flags the developer used locally. The server runs its own checks, period.

Three server-side hooks exist:

  • pre-receive — runs once per push, can reject the ENTIRE push
  • update — runs once per ref being pushed, can reject individual refs
  • post-receive — runs after push is accepted, cannot reject (for notifications/deployments)

Platform support:

  • GitHub.com — you CANNOT add custom server hooks. Use GitHub Actions, branch protection rules, and required status checks instead.
  • Self-hosted Git (GitLab CE, Gitea, Gogs, raw git daemon) — you CAN add custom server hooks in .git/hooks/ on the server.

Server hooks are the ultimate enforcement mechanism. They protect the shared repository from bad code, policy violations, and accidental damage.

# Client-side hooks: developer can bypass
git commit --no-verify -m "bad code"
git push --no-verify origin main
# Hooks SKIPPED! Bad code reaches the server!

# Server-side hooks: CANNOT be bypassed
git push origin main
# remote: ❌ Direct push to main is not allowed
# To github.com:team/repo.git
#  ! [remote rejected] main -> main (pre-receive hook declined)
# error: failed to push some refs

# Even --no-verify doesn't help:
git push --no-verify origin main
# --no-verify only skips LOCAL hooks
# Server hooks still run on the SERVER
# remote: ❌ Direct push to main is not allowed
# Push REJECTED by server!
Think of it this way: Client-side hooks are like personal rules — "I should run tests before committing." Server-side hooks are like border control — "You CANNOT enter without a valid passport." No amount of personal rule-breaking gets you past border control. --no-verify is ignoring your own rules. Server hooks are the border.
02pre-receive: The Atomic Gatekeeper

The pre-receive hook is the most powerful server-side hook. It runs once per push operation, BEFORE any refs are updated on the server.

It receives data on stdin — one line per ref being pushed. Each line has the format:

<old-sha> <new-sha> <ref-name>

Example stdin for a push that updates main and creates a feature branch:

a1b2c3d4e5f6... 7a8b9c0d1e2f... refs/heads/main
000000000000... f1e2d3c4b5a6... refs/heads/feature-x

The all-zeros SHA means "no previous value" — this is a new branch.

Key behavior: If pre-receive exits non-zero, the ENTIRE push is rejected. All refs in that push fail. Not just one — ALL of them. This is atomic enforcement.

Common use cases:

  • Block direct pushes to main — require pull requests instead
  • Reject force pushes — protect history integrity
  • Validate commit messages — enforce conventional commits format
  • Block large files — reject files over a size threshold
  • Enforce author identity — only specific email addresses allowed

Location on the server: .git/hooks/pre-receive (must be executable).

#!/bin/bash
# .git/hooks/pre-receive
# Block direct pushes to main branch

while read oldrev newrev refname; do
  if [ "$refname" = "refs/heads/main" ]; then
    echo "❌ ERROR: Direct push to main is not allowed."
    echo "Use a feature branch and create a pull request."
    exit 1
  fi
done

# If we reach here, all refs are safe
exit 0
💡 Pro Tip: The pre-receive hook reads ALL refs from stdin in a loop. If ANY ref violates your policy, exit 1 immediately. The entire push gets rejected as one atomic unit. This prevents partial pushes where some refs go through and others don't — it's all or nothing.
03update: Per-Ref Enforcement

The update hook is like pre-receive's per-ref sibling. It runs once per ref being pushed — not once per push operation.

Unlike pre-receive, it receives its data as command-line arguments, not stdin:

update <ref-name> <old-sha> <new-sha>

The critical difference: If update exits non-zero, only THAT specific ref is rejected. Other refs in the same push still go through.

This makes update ideal for per-branch rules where you want to reject individual branches but let others pass.

Use cases where update beats pre-receive:

  • Per-branch protection rules — block main but allow feature branches in the same push
  • Branch naming conventions — reject branches that don't match naming patterns
  • Delete protection — prevent deletion of important branches (new-sha = all zeros)
  • Tag protection — prevent tag deletion or modification
#!/bin/bash
# .git/hooks/update
# Per-ref enforcement

refname="$1"
oldrev="$2"
newrev="$3"

# Block direct pushes to main
if [ "$refname" = "refs/heads/main" ]; then
  echo "❌ Direct push to main is not allowed."
  echo "Use a pull request instead."
  exit 1
fi

# Block force pushes to release branches
if [[ "$refname" == refs/heads/release/* ]]; then
  # Check if this is a force push (non-fast-forward)
  merge_base=$(git merge-base "$oldrev" "$newrev" 2>/dev/null)
  if [ "$merge_base" != "$oldrev" ]; then
    echo "❌ Force push to $refname is not allowed."
    exit 1
  fi
fi

# Prevent deleting protected tags
if [[ "$refname" == refs/tags/* ]] && [ "$newrev" = "0000000000000000000000000000000000000000" ]; then
  echo "❌ Tag deletion is not allowed."
  exit 1
fi

exit 0
pre-receive vs update — when to use which? Use pre-receive when you want atomic enforcement — if ANY ref is bad, reject ALL. Use update when you want per-ref enforcement — reject the bad ref, let the good ones through. Most teams use pre-receive for simplicity. Advanced setups use both: pre-receive for global rules, update for per-branch rules.
04post-receive: Notifications & Automation

The post-receive hook runs AFTER all refs have been accepted and updated on the server. The push has already succeeded — this hook cannot reject it.

Like pre-receive, it receives data on stdin — one line per ref:

<old-sha> <new-sha> <ref-name>

Since the push is already accepted, post-receive is used for side effects and notifications:

  • Trigger CI/CD pipelines — tell Jenkins, GitHub Actions, or GitLab CI that new code is available
  • Send notifications — Slack, email, or webhook alerts for pushes
  • Auto-deploy — update a staging server when main is pushed
  • Update a checkout — keep a working directory in sync (common for web servers)
  • Log push activity — audit trail of who pushed what and when
  • Mirror to another repository — push to backup or mirror repos
#!/bin/bash
# .git/hooks/post-receive
# Auto-deploy to staging when main is updated

while read oldrev newrev refname; do
  if [ "$refname" = "refs/heads/main" ]; then
    echo "📦 Deploying main to staging..."

    # Checkout the latest main to web directory
    GIT_WORK_TREE=/var/www/staging git checkout -f main

    # Run deployment steps
    cd /var/www/staging
    npm install --production
    npm run build
    pm2 restart app-staging

    echo "✅ Staging deployed successfully!"

    # Send notification
    curl -s -X POST "https://hooks.slack.com/services/XXX" \
      -H "Content-Type: application/json" \
      -d '{"text":"🚀 Staging deployed from main"}'
  fi
done
💡 Pro Tip: post-receive is the self-hosted alternative to GitHub Actions' "on: push" trigger. On GitHub.com, you configure webhooks and Actions. On self-hosted Git, post-receive is your automation engine. The pattern is the same: new code arrives → something happens automatically.
05Platform Support: GitHub vs Self-Hosted

Not all platforms let you install server-side hooks. Here is how major platforms handle server-side enforcement:

GitHub.com — No custom server hooks. Period. GitHub replaces them with:

  • Branch protection rules — require PRs, require reviews, block force pushes (Settings → Branches)
  • Required status checks — CI must pass before merge
  • GitHub Actions — custom workflows triggered on push, PR, etc. (like post-receive but more powerful)
  • Webhooks — HTTP notifications for push, PR, and other events

GitLab CE/EE (self-hosted) — Full server hook support:

  • Custom hooks in /opt/gitlab/embedded/service/gitlab-shell/hooks/ (global) or <project>.git/custom_hooks/ (per-project)
  • Push rules (GitLab UI) — commit message regex, author email regex, file size limits
  • GitLab CI — pipeline automation

Gitea/Gogs — Lightweight, supports custom hooks:

  • <repo>/hooks/ directory for pre-receive, update, post-receive
  • Web-based hook configuration in UI

Bitbucket Server — Plugin system:

  • Merge checks via plugins (like server hooks but configured in UI)
  • Hook SDK for custom Java-based hooks

The industry pattern is clear: SaaS platforms replace raw server hooks with web-based configuration. Self-hosted gives you raw power. SaaS gives you convenience and guardrails.

# GitHub.com — no server hooks, use these instead:

# 1. Branch protection (Settings → Branches → Add rule)
# - Require pull request before merging
# - Require status checks to pass
# - Require signed commits
# - Do not allow force pushes

# 2. GitHub Actions (like post-receive but better)
# .github/workflows/ci.yml
# name: CI
# on:
#   push:
#     branches: [main]
#   pull_request:
#     branches: [main]

# GitLab CE — server hooks work!
sudo -u git cp pre-receive \
  /opt/gitlab/embedded/service/gitlab-shell/hooks/
sudo chmod +x /opt/gitlab/.../hooks/pre-receive

# Per-project hook:
sudo -u git cp pre-receive \
  /var/opt/gitlab/git-data/repositories/@hashed/ab/cd/abcd.git/custom_hooks/
sudo chmod +x .../custom_hooks/pre-receive
💡 Decision Framework: If you're on GitHub.com, stop looking for server hooks — they don't exist. Use branch protection + GitHub Actions. If you're self-hosting GitLab, you get BOTH: server hooks AND push rules. If you're on Gitea/Gogs, you get server hooks. Choose based on your platform, not your preference.

Lo kar liya — Key Points:

  • ✅ Server-side hooks run on the Git SERVER — developers cannot bypass them with --no-verify
  • ✅ Three server-side hooks: pre-receive (once per push), update (once per ref), post-receive (after acceptance)
  • ✅ pre-receive rejects the ENTIRE push if it exits non-zero — atomic all-or-nothing enforcement
  • ✅ update rejects individual refs — other refs in the same push still go through
  • ✅ post-receive runs after the push is accepted — used for notifications, CI triggers, and auto-deploy
  • ✅ GitHub.com does NOT support custom server hooks — use Branch Protection + GitHub Actions instead
  • ✅ Self-hosted Git (GitLab CE, Gitea) supports custom server hooks in .git/hooks/ on the server
Course Search
Search across all chapters & stages
📖

Search the course

Type any topic — branching, stash, rebase, hooks — and jump straight to that chapter.

merge branchesgit stashundo commitrebase