Server-Side Hooks: Push-Validierung zentral steuern
git
HEAD
Git · Server-Side Hooks · GitLab · GitHub
Server-Side Hooks: Push-Validierung zentral steuern
Regeln, die kein Entwickler lokal umgehen kann

Client-seitige Git Hooks sind bequem, aber jeder Entwickler kann sie mit --no-verify umgehen, löschen oder einfach nie installieren. Server-Side Hooks laufen dagegen auf dem zentralen Git-Server, wo niemand direkten Zugriff hat, und werden damit zum einzigen echten Durchsetzungspunkt für Force-Push-Schutz, Dateigrößen-Limits und Branch-Namenskonventionen im Team.

14 Min. Lesezeit pre-receive · update · post-receive GitLab Push Rules · GitHub Rulesets

1. Warum Server-Side Hooks der eigentliche Durchsetzungspunkt sind

Client-Side Hooks wie pre-commit und commit-msg liegen im lokalen .git/hooks-Verzeichnis oder werden über Husky per npm install eingerichtet. Genau darin liegt ihre strukturelle Schwäche: Jeder Entwickler kann sie mit git commit --no-verify umgehen, die Skripte löschen oder ein frisches Repository klonen, ohne dass Husky jemals installiert wurde. Client-Side Hooks sind damit eine Komfort- und Frühwarnschicht, keine Sicherheitsgrenze - sie fangen Tippfehler und offensichtliche Fehler ab, bevor überhaupt gepusht wird, aber sie garantieren nichts.

Server-Side Hooks laufen dagegen auf dem zentralen Git-Server, sei es ein selbst gehosteter bare-Repository-Server, eine GitLab-Instanz oder ein GitHub-Enterprise-Server. Kein Entwickler hat direkten Zugriff auf dieses Verzeichnis, kann die Skripte nicht lokal deaktivieren und kann den Push nicht ohne Umweg über administrative Rechte manipulieren. Genau das macht Server-Side Hooks zum einzigen Ort, an dem Regeln wie Force-Push-Verbote, Dateigrößen-Limits oder Branch-Namenskonventionen tatsächlich für jeden Client gelten, unabhängig davon, ob er über die Kommandozeile, ein IDE-Plugin oder ein CI-System pusht.

2. Die drei Hook-Typen im Git-Push-Ablauf

Git bietet auf der Server-Seite drei relevante Hook-Typen, die in einer festen Reihenfolge während eines git push ausgeführt werden: pre-receive, update und post-receive. Alle drei liegen im hooks/-Verzeichnis des Remote-Repositorys und müssen ausführbar sein, um zu greifen. Der entscheidende Unterschied liegt im Zeitpunkt der Ausführung und in der Fähigkeit, den Push tatsächlich abzulehnen.

pre-receive läuft genau einmal für den gesamten Push, bevor irgendeine Referenz aktualisiert wird, und erhält über stdin Zeilen im Format <alte-sha> <neue-sha> <ref-name> für jeden betroffenen Branch oder Tag. update läuft dagegen einmal pro zu aktualisierender Referenz und erhält drei positionale Argumente: Refname, alte SHA und neue SHA. post-receive schließlich läuft erst, nachdem alle Referenzen bereits aktualisiert wurden - er eignet sich für Benachrichtigungen und CI-Trigger, kann den Push aber nicht mehr zurückweisen, weil die Änderung zu diesem Zeitpunkt bereits im Repository steht.

3. pre-receive: Push blockieren, bevor sich etwas ändert

Der pre-receive-Hook ist der mächtigste Ansatzpunkt für harte Validierungen, weil er als einziger Hook den gesamten Push in einem Rutsch sieht und ihn mit einem Nicht-Null-Exit-Code komplett ablehnen kann, ohne dass auch nur eine einzige Referenz aktualisiert wird. Das macht ihn ideal für Prüfungen, die alle betroffenen Branches gemeinsam betrachten müssen, etwa ein globales Force-Push-Verbot auf geschützte Branches oder eine Prüfung, ob überhaupt ein Merge-Commit im Push enthalten ist.

Technisch liest das Skript die stdin-Zeilen zeilenweise und prüft für jede Referenz, ob die alte SHA ein Vorfahre der neuen SHA ist. Ist das nicht der Fall, handelt es sich um einen Force-Push, der die Historie umschreibt statt sie zu erweitern. git merge-base --is-ancestor <alte-sha> <neue-sha> ist genau dafür gebaut: Der Befehl gibt Exit-Code 0 zurück, wenn die alte SHA ein Vorfahre ist, und ungleich 0, wenn nicht. Läuft die Prüfung auf einem geschützten Branch wie main oder production und schlägt sie fehl, gibt das Skript eine Fehlermeldung nach stderr aus und beendet sich mit Exit-Code 1, was Git den kompletten Push ablehnen lässt.


#!/usr/bin/env bash
# pre-receive hook: reject force-pushes to protected branches
set -euo pipefail

readonly PROTECTED_BRANCHES="main|production|release/.*"

while read -r old_sha new_sha ref_name; do
  branch="${ref_name#refs/heads/}"

  # Skip branch deletions and non-matching branches
  [[ "$new_sha" =~ ^0+$ ]] && continue
  [[ "$branch" =~ ^($PROTECTED_BRANCHES)$ ]] || continue

  # New branch creation on a protected name is fine
  [[ "$old_sha" =~ ^0+$ ]] && continue

  # Force-push means old_sha is NOT an ancestor of new_sha
  if ! git merge-base --is-ancestor "$old_sha" "$new_sha" 2>/dev/null; then
    echo "REJECTED: force-push to protected branch '$branch' is not allowed" >&2
    echo "History rewrites on $branch must go through a reviewed process" >&2
    exit 1
  fi
done

exit 0

4. update: Regeln pro Branch durchsetzen

Während pre-receive alle Referenzen im Push gemeinsam sieht, wird update für jede einzelne Referenz separat aufgerufen, mit dem Refname, der alten SHA und der neuen SHA als positionale Argumente $1, $2 und $3. Das macht ihn zum natürlichen Ort für Regeln, die pro Branch entschieden werden, ohne den gesamten Push-Kontext betrachten zu müssen, etwa eine Prüfung der Branch-Namenskonvention beim Anlegen eines neuen Branches.

Ein typisches Beispiel: Ein Team erzwingt, dass neue Branches einem festen Präfix-Schema folgen, etwa feature/*, bugfix/* oder hotfix/*, damit CI-Pipelines und Deployment-Skripte anhand des Namens automatisch das richtige Verhalten wählen können. Der update-Hook prüft den Refname mit einem regulären Ausdruck und lehnt die Referenz mit Exit-Code ungleich 0 ab, wenn sie nicht passt - anders als bei pre-receive betrifft die Ablehnung dann aber nur diese eine Referenz, nicht zwingend den gesamten Push, falls mehrere Referenzen gleichzeitig aktualisiert werden.


#!/usr/bin/env bash
# update hook: enforce branch naming convention
# Arguments: $1 = refname, $2 = old_sha, $3 = new_sha
set -euo pipefail

refname="$1"
old_sha="$2"
new_sha="$3"

# Only enforce naming on newly created branches
if [[ "$old_sha" =~ ^0+$ ]] && [[ "$refname" =~ ^refs/heads/ ]]; then
  branch="${refname#refs/heads/}"

  # Allow protected long-lived branches by exact name
  case "$branch" in
    main|develop|production) exit 0 ;;
  esac

  if ! [[ "$branch" =~ ^(feature|bugfix|hotfix|release)/[a-z0-9._-]+$ ]]; then
    echo "REJECTED: branch '$branch' violates naming convention" >&2
    echo "Use one of: feature/<name>, bugfix/<name>, hotfix/<name>, release/<name>" >&2
    exit 1
  fi
fi

exit 0

5. post-receive: Benachrichtigungen und CI-Trigger

post-receive läuft, nachdem alle Referenzen bereits erfolgreich aktualisiert wurden, und erhält wie pre-receive die Liste der Änderungen über stdin. Der entscheidende Unterschied: Dieser Hook kann den Push nicht mehr zurückweisen, egal welchen Exit-Code er zurückgibt - die Commits sind zu diesem Zeitpunkt bereits fester Bestandteil des Repositorys. Das macht ihn ungeeignet für Validierung, aber ideal für alles, was nach einer erfolgreichen Änderung passieren soll.

Typische Anwendungsfälle sind das Auslösen einer CI-Pipeline über einen Webhook-Aufruf, das Versenden einer Slack- oder E-Mail-Benachrichtigung an das Team, das Aktualisieren eines Deployment-Dashboards oder das Triggern eines automatischen Deployments bei Pushes auf main. Weil post-receive nach dem eigentlichen Push-Vorgang läuft, blockiert ein langsamer Webhook-Aufruf zwar die Antwort an den Client, gefährdet aber nicht mehr die Integrität des Repositorys - im Zweifel sollte der Hook zeitaufwändige Arbeit asynchron im Hintergrund anstoßen, statt den Client minutenlang auf eine Antwort warten zu lassen.

6. GitLab Push Rules als Managed-Alternative

Wer eine GitLab-Instanz betreibt, muss pre-receive und update nicht zwangsläufig als rohes Shell-Skript pflegen. GitLab bietet mit Push Rules eine UI-gestützte Konfigurationsebene, die intern serverseitig durchgesetzt wird und damit dieselbe Sicherheitsgarantie wie ein selbst geschriebener pre-receive-Hook bietet. Push Rules lassen sich pro Projekt oder global auf Instanz-Ebene definieren und decken unter anderem reguläre Ausdrücke für erlaubte Branch-Namen, Commit-Message-Formate, maximale Dateigröße und das Verbot von Force-Pushes auf bestimmte Branches ab.

Der Vorteil gegenüber einem eigenen Skript liegt in der Wartbarkeit: Änderungen an den Regeln erfordern keinen Zugriff auf den Server und kein Deployment eines neuen Hook-Skripts, sondern werden über die Projekteinstellungen gepflegt und sind versioniert im Audit-Log sichtbar. Für komplexere Logik, die über reguläre Ausdrücke hinausgeht, etwa eine Prüfung gegen eine externe Datenbank oder ein Ticket-System, bleibt ein klassischer pre-receive-Hook auf selbst gehosteten GitLab-Instanzen weiterhin verfügbar und lässt sich mit Push Rules kombinieren.


# GitLab Push Rules configuration (Project Settings > Repository > Push Rules)
# These map to fields in the GitLab API / gitlab.rb for self-managed instances

push_rules:
  # Reject commits that don't match this branch naming pattern
  branch_name_regex: "^(feature|bugfix|hotfix|release)/[a-z0-9._-]+$"

  # Reject force-pushes on protected branches (also configurable via
  # Settings > Repository > Protected Branches > Allowed to force push: No)
  deny_delete_tag: true
  member_check: true

  # Reject files above this size in newly pushed commits (MB)
  max_file_size: 20

  # Reject commit messages that don't reference a ticket ID
  commit_message_regex: "^(feat|fix|chore|docs)(\\(.+\\))?: .+ \\[[A-Z]+-[0-9]+\\]$"

  # Reject pushes that add files matching these paths (e.g. secrets)
  file_name_regex: "(^|/)\\.env$|\\.pem$|id_rsa$"

7. GitHub Rulesets und Branch Protection

Rohe pre-receive-Hook-Skripte, wie sie GitLab und selbst gehostete Git-Server unterstützen, gibt es bei GitHub nur in GitHub Enterprise Server, der selbst gehosteten Variante. Auf GitHub.com, dem SaaS-Angebot, gibt es keinen Zugriff auf den Git-Server selbst und damit keine Möglichkeit, eigene Hook-Skripte zu deployen. Stattdessen stellt GitHub.com mit Repository Rulesets und dem älteren Branch Protection eine vergleichbare, aber deklarative Alternative bereit, die serverseitig ohne eigenen Code durchgesetzt wird.

Rulesets lassen sich über die Weboberfläche, per gh api oder als Infrastructure-as-Code über Terraform verwalten und decken unter anderem Pflicht-Statuschecks vor dem Merge, Force-Push-Verbote, das Verbot von Löschungen geschützter Branches und die Anforderung signierter Commits ab. Anders als bei GitLab Push Rules gibt es hier keine Möglichkeit, beliebige Shell-Logik einzubinden - wer komplexere serverseitige Prüfungen braucht, muss diese über verpflichtende Status-Checks abbilden, die von einer externen CI-Pipeline oder GitHub Action gesetzt werden, bevor ein Merge erlaubt ist.


# Create a GitHub repository ruleset via gh CLI (GitHub.com or Enterprise Cloud)
# Enforces required status checks and blocks force-pushes on main

gh api repos/mironsoft/shop-backend/rulesets \
  --method POST \
  -f name='protect-main' \
  -f target='branch' \
  -f enforcement='active' \
  -f 'conditions[ref_name][include][]=refs/heads/main' \
  -f 'rules[][type]=deletion' \
  -f 'rules[][type]=non_fast_forward' \
  -f 'rules[][type]=required_status_checks' \
  -f 'rules[][parameters][required_status_checks][][context]=ci/tests' \
  -f 'rules[][parameters][required_status_checks][][context]=ci/phpstan' \
  -f 'rules[][parameters][strict_required_status_checks_policy]=true'

# non_fast_forward = reject force-pushes server-side, equivalent to
# checking old_sha is an ancestor of new_sha in a raw pre-receive hook

8. Praxisbeispiele: Force-Push, Dateigröße, Branch-Namen

Neben Force-Push-Schutz und Branch-Namenskonventionen ist die Ablehnung großer Dateien einer der häufigsten Anwendungsfälle für pre-receive. Binärdateien, versehentlich committete .env-Dateien mit Zugangsdaten oder große Media-Assets sollen erst gar nicht ins Repository gelangen, statt später mühsam mit git filter-repo aus der Historie entfernt zu werden. Der Hook nutzt dafür git diff-tree, um die im Push neu hinzugekommenen Objekte zu ermitteln, und git cat-file -s, um deren Größe zu prüfen, ohne den vollständigen Blob-Inhalt laden zu müssen.

Alle drei Beispiele, Force-Push-Schutz, Branch-Namen und Dateigrößen-Limits, teilen dasselbe Grundprinzip: Die Prüfung inspiziert ausschließlich Metadaten und Objekte, die Git dem Hook über stdin, Argumente oder Git-Kommandos direkt zur Verfügung stellt, ohne dass der Client-seitige Zustand relevant ist. Das ist der entscheidende Unterschied zu einem Linter, der im pre-commit-Hook läuft: Server-Side Hooks sehen nur, was tatsächlich im Repository landen würde, nicht, was ein Entwickler lokal im Arbeitsverzeichnis liegen hat.


#!/usr/bin/env bash
# pre-receive hook: reject files above a size threshold
set -euo pipefail

readonly MAX_SIZE_BYTES=$((10 * 1024 * 1024))  # 10 MB

while read -r old_sha new_sha ref_name; do
  [[ "$new_sha" =~ ^0+$ ]] && continue
  base_sha="$old_sha"
  [[ "$base_sha" =~ ^0+$ ]] && base_sha="$(git rev-list --max-parents=0 "$new_sha" | tail -1)"

  # List new blob objects introduced by this push
  while read -r blob_sha; do
    size=$(git cat-file -s "$blob_sha")
    if (( size > MAX_SIZE_BYTES )); then
      path=$(git rev-list --objects "$base_sha..$new_sha" | grep "$blob_sha" | cut -d' ' -f2-)
      echo "REJECTED: '$path' is ${size} bytes, exceeds ${MAX_SIZE_BYTES} byte limit" >&2
      exit 1
    fi
  done < <(git rev-list --objects "$base_sha..$new_sha" \
            | git cat-file --batch-check='%(objectname) %(objecttype)' \
            | awk '$2 == "blob" {print $1}')
done

exit 0

9. Client-Side vs. Server-Side im direkten Vergleich

Client-Side Hooks, selbst geschriebene Server-Side Hooks und Managed-Lösungen wie GitLab Push Rules oder GitHub Rulesets lösen ähnliche Probleme mit sehr unterschiedlichen Garantien. Die folgende Übersicht zeigt, warum die Wahl der Ebene keine Stilfrage, sondern eine Sicherheitsentscheidung ist.

Dimension Client-Side Hooks Eigener Server-Side Hook GitLab/GitHub Managed Rules
Lokal umgehbar Ja, --no-verify oder löschen Nein Nein
Gilt für alle Clients Nur wenn installiert (Husky + npm install) Ja, immer Ja, immer
Setup-Aufwand Gering, aber pro Entwickler nötig Hoch, eigenes Skript + Server-Zugriff Gering, UI-Konfiguration
Freie Skript-Logik Voll frei Voll frei Begrenzt auf vordefinierte Regeln
Frühzeitiges Feedback Sofort, vor dem Commit Erst beim Push Erst beim Push
Wartung bei Regeländerung Rollout an alle Entwickler nötig Deployment auf Git-Server nötig Sofort über Projekteinstellungen

Die Tabelle macht deutlich, dass Client-Side und Server-Side Hooks keine Konkurrenten, sondern komplementäre Schichten sind. Client-Side Hooks liefern schnelles Feedback direkt beim Committen und sparen unnötige, von vornherein abgelehnte Pushes. Server-Side Hooks oder Managed Rules stellen sicher, dass diese Regeln tatsächlich für jeden gelten, unabhängig davon, ob die lokale Konfiguration korrekt oder überhaupt vorhanden ist.

Mironsoft

Git-Workflows, Server-Hooks und CI/CD-Absicherung für Entwicklerteams

Push-Validierung, die wirklich für jeden gilt?

Wir richten Server-Side Hooks und Managed Rules für euer Team ein, von Force-Push-Schutz über Branch-Konventionen bis zu Dateigrößen-Limits - zugeschnitten auf GitLab, GitHub oder euren selbst gehosteten Git-Server.

Hook-Entwicklung

pre-receive, update und post-receive Skripte für individuelle Regeln

GitLab/GitHub Setup

Push Rules, Rulesets und Branch Protection sauber konfigurieren

CI/CD-Integration

Pflicht-Statuschecks und automatisierte Freigabe-Workflows

10. Zusammenfassung

Server-Side Hooks lösen ein Problem, das Client-Side Hooks strukturell nicht lösen können: die verlässliche Durchsetzung von Regeln, unabhängig davon, wie ein Entwickler pusht. pre-receive sieht den gesamten Push vor jeder Änderung und kann ihn komplett ablehnen, etwa bei einem Force-Push auf main oder bei zu großen Dateien. update prüft jede Referenz einzeln, etwa gegen eine Branch-Namenskonvention. post-receive läuft nach der Änderung und eignet sich für Benachrichtigungen und CI-Trigger, kann den Push aber nicht mehr zurückweisen.

Wer keine eigenen Shell-Skripte pflegen will, findet in GitLab Push Rules und GitHub Rulesets eine deklarative, UI-gestützte Alternative mit derselben serverseitigen Durchsetzungsgarantie. Die beste Strategie kombiniert beide Ebenen: Client-Side Hooks für schnelles Feedback direkt beim Committen, Server-Side Hooks oder Managed Rules als unumgehbare Absicherung dahinter. Nur die Kombination stellt sicher, dass Regeln nicht nur dokumentiert, sondern tatsächlich durchgesetzt werden.

Server-Side Hooks - Das Wichtigste auf einen Blick

Echter Durchsetzungspunkt

Server-Side Hooks laufen dort, wo kein Entwickler direkten Zugriff hat - im Gegensatz zu --no-verify bei Client-Side Hooks.

pre-receive vs. update vs. post-receive

pre-receive prüft den gesamten Push, update prüft pro Referenz, post-receive läuft danach und kann nicht mehr ablehnen.

Force-Push-Schutz

git merge-base --is-ancestor erkennt zuverlässig, ob ein Push die Historie umschreibt.

Managed Alternativen

GitLab Push Rules und GitHub Rulesets ersetzen rohe Skripte durch UI-Konfiguration mit derselben Garantie.

11. FAQ: Server-Side Hooks für Push-Validierung

1Was ist der Unterschied zwischen Client-Side und Server-Side Hooks?
Client-Side Hooks laufen lokal und lassen sich mit --no-verify umgehen. Server-Side Hooks laufen zentral, wo kein Entwickler direkten Zugriff hat, und sind der einzige echte Durchsetzungspunkt.
2Warum können Client-Side Hooks umgangen werden?
--no-verify überspringt sie explizit, sie lassen sich löschen, und bei Husky greifen sie erst nach npm install - ohne diesen Schritt existieren sie im geklonten Repository gar nicht.
3Was ist der Unterschied zwischen pre-receive, update und post-receive?
pre-receive sieht den gesamten Push vorab über stdin, update läuft pro Referenz mit refname/alter/neuer SHA, post-receive läuft danach für Benachrichtigungen und CI-Trigger.
4Kann pre-receive einen Push tatsächlich ablehnen?
Ja, ein Exit-Code ungleich 0 lehnt den kompletten Push ab, bevor eine Referenz aktualisiert wird. Der stärkste Hook für harte Validierungsregeln.
5Wie hängen GitLab Push Rules mit Server-Side Hooks zusammen?
Push Rules sind eine UI-Konfiguration, die intern wie ein pre-receive-Hook durchgesetzt wird - ohne eigenes Skript zu pflegen. Komplexere Logik lässt sich weiterhin per eigenem Hook ergänzen.
6Unterstützt GitHub rohe Server-Side Hooks?
Nur GitHub Enterprise Server. Auf GitHub.com gibt es keinen Server-Zugriff, stattdessen kommen Repository Rulesets und Branch Protection zum Einsatz.
7Wie blockiere ich Force-Pushes auf main serverseitig?
git merge-base --is-ancestor prüft, ob die alte SHA Vorfahre der neuen ist. Ist das nicht der Fall, lehnt der Hook mit Exit-Code 1 ab. GitLab/GitHub bieten dafür auch fertige Optionen.
8Wie erzwinge ich Branch-Namenskonventionen serverseitig?
Der update-Hook prüft neue Branch-Namen gegen einen regulären Ausdruck wie ^(feature|bugfix|hotfix)/. GitLab Push Rules bieten dafür ein direktes Regex-Feld.
9Verlangsamen Server-Side Hooks den Push spürbar?
Gut geschriebene Hooks laufen in Millisekunden bis wenigen Sekunden. Teure Operationen wie ein vollständiger Repository-Klon im Hook sollten vermieden werden.
10Wie ergänzen sich Server-Side Hooks mit commit-msg?
commit-msg gibt sofortiges lokales Feedback und reduziert unnötige Pushes. Server-Side Hooks stellen sicher, dass dieselben Regeln unabhängig von der lokalen Konfiguration tatsächlich gelten.