Server-Side Hooks
Client-side hooks? --no-verify se bypass. Server-side hooks? BYPASS NAHI HOGA. Yeh asli enforcement hai.
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!
--no-verify is ignoring your own rules. Server hooks are the border.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
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
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
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
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
Want to track your progress?
Log in to save your place and pick up where you left off.
Progress track karna chahte ho?
Login karo apni progress save karne ke liye aur jahan chhoda tha wahan se shuru karo.
Login