Chapter 8.4 — pre-push Hook☕ 13 min read

pre-push Hook

pre-push = local checkpoint. CI = server checkpoint. Dono lagao, protection strong rakho.

01pre-push: The Final Local Checkpoint

The pre-push hook runs when you execute git push, BEFORE any data is sent to the remote server.

It is your last quality gate before code leaves your machine and enters the shared repository. Once data reaches the remote, it affects your entire team — broken code, failing tests, direct pushes to main — all of it.

Unlike pre-commit (which must be FAST — under 5 seconds), pre-push can afford to be SLOWER. Why? Because push happens far less frequently than commit. A developer might commit 20 times but push only once. So 30-60 second checks are acceptable.

Common uses for pre-push:

  • Run full test suite — unit tests + integration tests before code reaches remote
  • Prevent direct push to main — block accidental pushes to protected branches
  • Check for large files — prevent accidentally pushing binaries or data files
  • Verify branch is up-to-date — warn if main has new commits you have not rebased on

If the hook exits with a non-zero code, the push is CANCELLED — no data is sent to the remote at all.

git push --no-verify bypasses this hook — the same escape hatch as other hooks. Use sparingly.

The hook receives information about what is being pushed on stdin: local ref, local SHA, remote ref, remote SHA. You can read this to make conditional checks based on the remote.

# Create a pre-push hook
cat > .git/hooks/pre-push << 'EOF'
#!/bin/bash

echo "🚀 Pre-push checks running..."

# Run tests before push
echo "🧪 Running tests..."
npm test
if [ $? -ne 0 ]; then
  echo "❌ Tests failed! Push blocked."
  exit 1
fi

echo "✅ All checks passed! Pushing..."
exit 0
EOF

chmod +x .git/hooks/pre-push

# Now when you push:
git push origin feature
# 🚀 Pre-push checks running...
# 🧪 Running tests...
# ... tests run ...
# ✅ All checks passed! Pushing...
# Data is sent to remote

# If tests fail:
git push origin feature
# ❌ Tests failed! Push blocked.
# No data sent to remote — your team is safe!
pre-push is your last LOCAL checkpoint. Once data leaves your machine, you cannot un-send it. The remote now has your broken code, and your team sees failing CI. pre-push catches problems BEFORE they affect anyone else. Think of it as the security check at the airport — you want to catch issues before boarding, not after landing.
02Preventing Direct Push to Main

One of the most valuable pre-push use cases: BLOCK direct pushes to main/master.

All changes to main should come through pull requests — direct pushes bypass code review. One accidental git push origin main can break production for everyone.

The hook checks which branch you are pushing to. If it is main, master, or develop, the push is blocked with a helpful message telling you to use a PR instead.

This is CLIENT-SIDE enforcement — developers can bypass with --no-verify. Server-side enforcement (GitHub branch protection) is the real guarantee. But the hook catches ACCIDENTAL pushes to main, which happen more often than you would think.

Pattern: git rev-parse --abbrev-ref HEAD gets the current branch name. Compare it against your list of protected branches.

# Prevent direct push to main
cat > .git/hooks/pre-push << 'EOF'
#!/bin/bash

BRANCH=$(git rev-parse --abbrev-ref HEAD)
PROTECTED_BRANCHES="main master develop"

for protected in $PROTECTED_BRANCHES; do
  if [ "$BRANCH" = "$protected" ]; then
    echo "❌ Direct push to $BRANCH is not allowed!"
    echo "   Create a feature branch and use a pull request."
    echo ""
    echo "   git checkout -b feature/your-feature"
    echo "   git push origin feature/your-feature"
    exit 1
  fi
done

exit 0
EOF

chmod +x .git/hooks/pre-push

# Try to push directly to main
git checkout main
git push origin main
# ❌ Direct push to main is not allowed!
# Push CANCELLED

# Correct way: use a feature branch
git checkout -b feature/new-api
git push origin feature/new-api
# ✅ Push proceeds (not a protected branch)
💡 Pro Tip: Protected branch checks in pre-push are your first line of defense, but not your only one. Always configure GitHub/GitLab branch protection rules on the server side. The hook catches accidents fast (instant feedback). The server catches intentional bypasses. Use both for defense in depth.
03Running Tests Before Push

pre-push is the right place to run your full test suite — it runs less often than pre-commit, so you can afford more thorough checks.

Unit tests + integration tests are appropriate for pre-push. Full E2E tests might be too slow — save those for CI.

Target time: under 60 seconds. If your test suite takes longer, developers will use --no-verify to skip it. A hook that everyone bypasses is worse than no hook at all.

If your test suite is slow, run only the RELEVANT tests based on what changed. Tools like Jest can run only tests related to changed files.

Always run tests on the CURRENT code, not just what is committed. Use git stash --keep-index if you need to test the staged version separately.

# Run test suite in pre-push
cat > .git/hooks/pre-push << 'EOF'
#!/bin/bash
set -e

echo "🧪 Running test suite..."

# Run unit tests (fast, ~30 seconds)
npm run test:unit
if [ $? -ne 0 ]; then
  echo "❌ Unit tests failed! Push blocked."
  exit 1
fi

# Run integration tests if available
if npm run test:integration 2>/dev/null; then
  echo "✅ Integration tests passed"
else
  echo "⚠️ No integration tests found, skipping"
fi

echo "✅ All tests passed! Safe to push."
exit 0
EOF

chmod +x .git/hooks/pre-push

# When you push:
git push origin feature
# 🧪 Running test suite...
# Unit tests: 45 passed, 0 failed
# Integration tests: 12 passed, 0 failed
# ✅ All tests passed! Safe to push.

# For slow test suites — run only relevant tests:
# npm run test:unit -- --changedSince=origin/main
04Verifying Branch Is Up-to-Date

A common problem: you are pushing a feature branch that is behind main. This causes merge conflicts on the PR and messy merge commits.

pre-push can check if your branch is behind the remote and warn you before you push.

It can also remind you to rebase before pushing to keep history clean and linear.

git fetch origin main && git log HEAD..origin/main --oneline — check if main has new commits that your branch does not have.

If main has new commits, suggest rebasing before push. This prevents the "merge main into feature" anti-pattern that pollutes history.

# Check if branch is behind main before push
cat > .git/hooks/pre-push << 'EOF'
#!/bin/bash
set -e

BRANCH=$(git rev-parse --abbrev-ref HEAD)

# Only check feature branches
if [ "$BRANCH" != "main" ] && [ "$BRANCH" != "master" ]; then
  # Fetch latest from remote
  git fetch origin main 2>/dev/null || true
  
  # Check if main has commits we lack
  NEW_COMMITS=$(git log HEAD..origin/main --oneline 2>/dev/null | wc -l)
  
  if [ "$NEW_COMMITS" -gt 0 ]; then
    echo "⚠️ Warning: origin/main has $NEW_COMMITS new commits"
    echo "   Consider rebasing before push:"
    echo "   git rebase origin/main"
    echo ""
    echo "   Continuing push anyway... (rebase recommended)"
  fi
fi

# Run tests
echo "🧪 Running tests..."
npm test --silent 2>/dev/null

exit 0
EOF

chmod +x .git/hooks/pre-push
💡 Pro Tip: The pre-push hook receives remote name and URL on stdin, not as arguments. To read them: read remote url. You can use this to apply different rules for different remotes (e.g., strict checks for production remote, relaxed for personal fork).
05pre-push vs CI: What Goes Where

pre-push: fast feedback on YOUR machine. Catches problems before they reach the remote.

CI (GitHub Actions): comprehensive checks on the SERVER. Catches problems that bypassed local hooks.

What belongs in pre-push: unit tests, linting (if not in pre-commit), branch protection checks, large file detection.

What belongs in CI only: E2E tests, security scans, performance benchmarks, cross-platform tests, deployment validation.

What belongs in BOTH: unit tests. pre-push catches failures fast locally, CI verifies nobody bypassed hooks.

Do not duplicate slow checks in pre-push that already run in CI — it wastes developer time. The goal is complementary coverage, not redundancy.

The goal: pre-push catches 80% of problems instantly. CI catches the remaining 20% and verifies the 80%.

# What goes where — decision guide

# PRE-COMMIT (< 5 seconds)
# - Lint staged files only
# - Format staged files only
# - Quick syntax checks

# PRE-PUSH (30-60 seconds)
# - Full unit test suite
# - Branch protection checks
# - Large file detection
# - Integration tests (if fast)

# CI / GITHUB ACTIONS (5-30 minutes)
# - Full test suite (unit + integration + E2E)
# - Security scans (SAST, dependency audit)
# - Performance benchmarks
# - Cross-platform tests
# - Deployment preview / staging

# Example: pre-push runs unit tests (30s)
# CI runs full suite + E2E + security (15min)
# Developer gets instant feedback locally
# PR still cannot merge until CI passes
pre-push hooks are most valuable when CI is slow. If your CI takes 15 minutes, catching a test failure locally in 30 seconds saves the entire team time. But if CI takes 2 minutes, pre-push test runs might not be worth the developer wait. Evaluate based on your CI speed and team preferences.

Lo kar liya — Key Points:

  • ✅ pre-push hook runs BEFORE git push sends data to the remote — your final local quality gate
  • ✅ It can be slower than pre-commit — push happens less frequently, so 30-60 second checks are acceptable
  • ✅ Most valuable uses: prevent direct push to main, run test suite, verify branch is up-to-date
  • ✅ Exit code 0 = allow push, non-zero = cancel push (no data sent to remote)
  • ✅ git push --no-verify bypasses pre-push — always pair with server-side branch protection
  • ✅ Keep test runs under 60 seconds — longer pushes frustrate developers into using --no-verify
  • ✅ pre-push catches problems fast locally; CI catches what bypasses hooks — use both for defense in depth
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