Git Hooks Grundlagen: Lokale Automatisierung
git
HEAD
Git · Hooks · Automatisierung · DevOps
Git Hooks Grundlagen: Lokale Automatisierung
Client- und Server-Hooks praxisnah verstehen

Git Hooks sind Skripte, die Git automatisch bei bestimmten Ereignissen wie Commit, Push oder Merge ausführt, lokal im Verzeichnis .git/hooks und serverseitig im blanken Repository. Dieser Artikel zeigt, wie sich Client und Server Hooks unterscheiden, wie ein pre-commit Hook Schritt für Schritt entsteht, wie Git Exit Codes interpretiert und wie core.hooksPath Hooks endlich teamweit versionierbar macht.

12 Min. Lesezeit pre-commit · pre-push · commit-msg core.hooksPath · pre-receive

1. Was Git Hooks sind und wo sie liegen

Ein Git Hook ist ein ausführbares Skript, das Git automatisch startet, sobald ein bestimmtes Ereignis im Repository eintritt, etwa ein Commit, ein Push oder ein Checkout. Technisch gesehen sind Hooks gewöhnliche Shell-Skripte oder Programme in einer beliebigen Sprache, solange die Datei ausführbar ist und mit einer korrekten Shebang-Zeile beginnt. Git ruft sie anhand eines festen, vordefinierten Namens wie pre-commit oder pre-push auf, sobald die passende Aktion stattfindet, ganz ohne zusätzliche Registrierung oder Konfigurationsdatei.

Jedes mit git init oder git clone erzeugte Repository enthält bereits ein Verzeichnis .git/hooks mit einer Reihe von Beispielskripten, die die Endung .sample tragen und deshalb standardmäßig inaktiv sind. Um einen Hook zu aktivieren, genügt es, die .sample-Endung zu entfernen und die Datei mit chmod +x ausführbar zu machen. Diese Einfachheit ist gleichzeitig die größte Stärke von Hooks: Es gibt kein Plugin-System, keine externe Abhängigkeit und keine zusätzliche Lernkurve, nur eine ausführbare Datei mit dem richtigen Namen am richtigen Ort im Dateisystem.


# View the .git/hooks directory of a freshly initialized repo
ls -la .git/hooks

# List available sample hooks (all carry the .sample extension)
ls .git/hooks/*.sample

# Activate a sample hook: strip .sample and make it executable
mv .git/hooks/pre-commit.sample .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit

# Verify Git recognizes the hook as executable
test -x .git/hooks/pre-commit && echo "pre-commit is active"

2. Client-seitige vs. serverseitige Hooks

Git unterscheidet grundsätzlich zwischen Client-Hooks, die auf dem Rechner eines einzelnen Entwicklers laufen, und Server-Hooks, die auf dem Repository ausgeführt werden, das Pushes empfängt, meist ein zentrales bare Repository auf einem Git-Server wie GitLab, Gitea oder einem selbst gehosteten Server. Zu den Client-Hooks gehören unter anderem pre-commit, commit-msg, pre-push, post-checkout und post-merge. Zu den Server-Hooks gehören pre-receive, update und post-receive, die alle beim Empfang eines Pushes auf dem Zielrepository ablaufen, nicht beim Entwickler.

Der praktische Unterschied ist entscheidend: Client-Hooks geben schnelles, lokales Feedback, können aber jederzeit mit git commit --no-verify umgangen werden oder fehlen komplett, weil sie wie im nächsten Abschnitt beschrieben standardmäßig nicht mit dem Repository geklont werden. Server-Hooks dagegen laufen auf Infrastruktur, die vom Team und nicht vom einzelnen Entwickler kontrolliert wird, und lassen sich von einem Entwickler-Client aus nicht umgehen. Wer eine Regel wirklich durchsetzen will, etwa das Verbot von Force-Pushes auf main, muss sie serverseitig implementieren, während Client-Hooks vor allem der Komfort- und Früherkennungsebene dienen.

3. Die wichtigsten Client-Hooks im Ueberblick

pre-commit läuft, bevor der Commit-Editor geöffnet wird, und ist der klassische Ort für Linting, Formatierungsprüfungen oder das Verbot von Debug-Ausgaben wie var_dump in gestagten PHP-Dateien. commit-msg läuft direkt danach und erhält als einziges Argument den Pfad zu einer temporären Datei mit der bereits eingegebenen Commit-Nachricht, ideal, um Formate wie Conventional Commits oder eine Pflicht-Ticket-Referenz zu erzwingen, bevor der Commit tatsächlich erstellt wird.

pre-push läuft, bevor Objekte zum Remote übertragen werden, und erhält über stdin eine Liste der betroffenen lokalen und remote Refs, was ihn zum passenden Ort für einen schnellen Testlauf oder eine lokale Prüfung gegen geschützte Branches macht. post-checkout läuft nach einem Branch-Wechsel und eignet sich, um auf neue Datenbank-Migrationen hinzuweisen. post-merge läuft nach einem erfolgreichen Merge oder Pull und wird häufig genutzt, um automatisch composer install auszuführen, wenn sich composer.lock im Merge geändert hat.

4. Einen pre-commit Hook Schritt für Schritt schreiben

Ein pre-commit Hook erhält keine Kommandozeilenargumente, sondern muss selbst ermitteln, welche Dateien gestaged sind. Der Standardbefehl dafür ist git diff --cached --name-only --diff-filter=ACM, der nur hinzugefügte, kopierte und geänderte Dateien liefert und gelöschte Dateien bewusst ausschließt, da ein Linter auf einer nicht mehr existierenden Datei ohnehin fehlschlagen würde. Aus dieser Dateiliste filtert man üblicherweise nach Endung, etwa nur .php-Dateien, bevor ein externes Tool wie phpcs oder php -l pro Datei aufgerufen wird.

Wichtig ist, den Hook mit der korrekten Shebang-Zeile zu beginnen, etwa #!/usr/bin/env bash, und am Ende explizit mit einem Exit-Code zu terminieren, wie im nächsten Abschnitt beschrieben. Ein häufiger Anfängerfehler ist, ausschließlich Dateien im Working Directory zu prüfen statt der tatsächlich gestagten Version, was bei teilweise gestagten Dateien mit git add -p zu falschen Ergebnissen führt. Die robuste Lösung prüft den gestagten Inhalt direkt über git show :datei.php statt die Datei einfach von der Festplatte zu lesen.


#!/usr/bin/env bash
set -euo pipefail

echo "Running pre-commit checks..."

# Collect staged PHP files (added, copied, modified), ignore deleted files
staged_php_files=$(git diff --cached --name-only --diff-filter=ACM -- '*.php')

if [ -z "$staged_php_files" ]; then
  echo "No staged PHP files, skipping lint."
  exit 0
fi

exit_code=0

for file in $staged_php_files; do
  # Lint the staged content, not the working-directory version
  if ! php -l "$file" > /dev/null 2>&1; then
    echo "Syntax error in: $file"
    exit_code=1
  fi

  # Block accidental debug output in committed code
  if git show ":$file" | grep -nE 'var_dump\(|dd\(|print_r\(' > /dev/null; then
    echo "Debug output found in: $file"
    exit_code=1
  fi
done

if [ "$exit_code" -ne 0 ]; then
  echo "pre-commit checks failed, commit aborted."
fi

exit $exit_code

5. Exit-Codes: Wie Git Hook-Ergebnisse interpretiert

Git wertet nach jedem Hook ausschließlich den Exit-Code aus, nicht die Textausgabe. Ein Exit-Code von 0 bedeutet Erfolg, Git fährt mit der Operation fort. Jeder andere Exit-Code, egal ob 1, 2 oder ein beliebiger anderer Wert zwischen 1 und 255, gilt als Fehlschlag und bricht bei den meisten Client-Hooks die zugehörige Git-Operation vollständig ab. Ein pre-commit Hook mit Exit-Code ungleich null verhindert also den Commit komplett, der Nutzer landet zurück im Terminal, ohne dass ein neuer Commit entsteht.

Nicht alle Hooks können die Operation blockieren: post-commit, post-checkout und post-merge laufen erst, nachdem die eigentliche Aktion bereits abgeschlossen ist, ihr Exit-Code hat deshalb keinen Einfluss mehr auf das Ergebnis, sondern dient nur der Information oder Folgeaktionen. Ein häufiger Fehler beim Schreiben eigener Hooks ist, sich implizit auf den Exit-Code des letzten Befehls zu verlassen, obwohl ein Zwischenbefehl bereits fehlgeschlagen ist. set -euo pipefail am Skriptanfang und ein expliziter exit-Aufruf am Ende schaffen hier Klarheit und verhindern, dass ein Hook fälschlicherweise Erfolg meldet.


#!/usr/bin/env bash
# Minimal example: explicit exit codes instead of relying on the last command

check_branch_name() {
  local branch
  branch=$(git symbolic-ref --short HEAD)

  if [[ "$branch" =~ ^(feature|bugfix|hotfix)/.+ ]]; then
    return 0
  fi

  echo "Branch name '$branch' does not match feature|bugfix|hotfix/*"
  return 1
}

if check_branch_name; then
  exit 0
else
  # Non-zero exit code tells Git to abort the operation
  exit 1
fi

6. Server-seitige Hooks am Beispiel pre-receive

pre-receive läuft auf dem Zielrepository, bevor irgendeine Ref aktualisiert wird, und ist der mächtigste Server-Hook, weil er einen kompletten Push atomar annehmen oder ablehnen kann, inklusive aller enthaltenen Commits. Git liefert über stdin pro aktualisierter Referenz eine Zeile mit drei durch Leerzeichen getrennten Werten: dem alten Objekt-Hash, dem neuen Objekt-Hash und dem vollständigen Ref-Namen wie refs/heads/main. Der Hook liest diese Zeilen und kann anhand der Werte beliebige Regeln prüfen, bevor Git die Referenzen tatsächlich aktualisiert.

Typische Einsatzzwecke sind das Verbot von Force-Pushes auf geschützte Branches, das serverseitige Erzwingen von Commit-Message-Konventionen, die Prüfung auf signierte Commits oder das Verhindern von direkten Pushes auf main außerhalb eines Merge-Request-Workflows. Der entscheidende Vorteil gegenüber Client-Hooks: Ein pre-receive Hook kann nicht mit --no-verify umgangen werden, weil er gar nicht auf dem Rechner des Entwicklers läuft, sondern ausschließlich auf dem Server, den das Team kontrolliert. Bei gehosteten Plattformen wie GitHub oder GitLab.com ist direkter Zugriff auf pre-receive meist eingeschränkt, weshalb dort Branch-Protection-Regeln oder serverseitige CI-Checks dieselbe Rolle übernehmen.


#!/usr/bin/env bash
# Server-side hook: runs on the bare repository before refs are updated

zero_sha="0000000000000000000000000000000000000000"

while read -r old_sha new_sha ref_name; do
  # Reject deletions and force-pushes on the protected main branch
  if [ "$ref_name" = "refs/heads/main" ]; then
    if [ "$new_sha" = "$zero_sha" ]; then
      echo "Rejected: deleting the main branch is not allowed."
      exit 1
    fi

    # A force-push means the new history does not contain the old tip
    if [ "$old_sha" != "$zero_sha" ] && ! git merge-base --is-ancestor "$old_sha" "$new_sha"; then
      echo "Rejected: force-push to main is not allowed."
      exit 1
    fi
  fi
done

exit 0

7. Warum Hooks standardmäßig nicht versioniert werden

Das Verzeichnis .git selbst ist niemals Teil des versionierten Projektinhalts, es ist der interne Metadaten-Speicher von Git und wird beim Klonen eines Repositories vollständig neu erzeugt, nicht kopiert. Alles, was in .git/hooks liegt, gilt deshalb als lokale, maschinenspezifische Konfiguration, genauso wie .git/config oder Remote-Tracking-Informationen. Klont ein neuer Kollege das Repository, erhält er ein frisches .git/hooks Verzeichnis mit ausschließlich den Standard-Beispieldateien, unabhängig davon, welche Hooks im Ursprungsrepository aktiv waren.

Diese Eigenschaft ist keine Einschränkung von Git, sondern folgt konsequent aus dem Zweck von .git: Hooks können beliebigen Code auf dem Zielsystem ausführen, und ein automatisch aktiviertes, aus dem Repository geklontes Skript wäre ein erhebliches Sicherheitsrisiko, vergleichbar mit dem automatischen Ausführen von heruntergeladenem Code. Aus genau diesem Grund gibt es keinen eingebauten Mechanismus, um Hooks aus .git/hooks zu versionieren. Ein manueller Workaround, etwa Skripte in einem README zu dokumentieren und Entwickler zu bitten, sie von Hand zu kopieren, ist fehleranfällig und wird in der Praxis regelmäßig vergessen, sobald ein neuer Hook hinzukommt oder ein bestehender sich ändert.

8. core.hooksPath: Hooks versionieren und teamweit verteilen

Die Git-Konfigurationsoption core.hooksPath, verfügbar seit Git 2.9, löst das Problem direkt an der Wurzel: Sie weist Git an, Hooks nicht mehr in .git/hooks zu suchen, sondern in einem beliebigen anderen Verzeichnis, das ganz normal Teil des versionierten Projekts sein kann, etwa .githooks/ im Projekt-Root. Sobald git config core.hooksPath .githooks gesetzt ist, ruft Git bei jedem passenden Ereignis die Skripte aus diesem Verzeichnis auf, exakt nach denselben Namens- und Ausführbarkeits-Regeln wie zuvor bei .git/hooks.

Der verbleibende manuelle Schritt ist die einmalige Ausführung des git config Befehls nach jedem Klonen, was sich über ein Setup-Skript, einen Makefile-Task oder eine Anweisung im README automatisieren lässt. Für Node.js- beziehungsweise npm-basierte Projekte übernehmen Tools wie Husky genau diese Aufgabe automatisch über ein prepare-Skript in der package.json, das bei jedem npm install läuft und das eigentliche Konfigurieren von core.hooksPath unsichtbar für den Entwickler erledigt. Wie Husky in Kombination mit lint-staged genau funktioniert und wie sich Hooks damit teamweit ohne manuellen Schritt durchsetzen lassen, behandelt ein eigener Folgeartikel im Detail.


# Versioned hooks directory inside the repository
mkdir -p .githooks

# Move the existing pre-commit hook into the versioned directory
mv .git/hooks/pre-commit .githooks/pre-commit
chmod +x .githooks/pre-commit

# Tell Git to look here instead of .git/hooks
git config core.hooksPath .githooks

# Commit the hooks directory like any other project file
git add .githooks
git commit -m "Add versioned pre-commit hook via core.hooksPath"

# Every teammate runs this once after cloning
git config core.hooksPath .githooks

9. Hook-Strategien im Vergleich

Die drei praxisrelevanten Strategien, um Hooks im Team konsistent einzusetzen, unterscheiden sich vor allem in Versionierbarkeit, Umgehbarkeit und Setup-Aufwand. Die folgende Uebersicht fasst zusammen, wann welcher Ansatz die richtige Wahl ist.

Ansatz Im Repo versioniert Umgehbar mit --no-verify Setup-Aufwand
Manuelles .git/hooks Nein Ja Keiner, aber wirkungslos nach jedem Klon
core.hooksPath Ja Ja Einmaliger git config Befehl pro Klon
Husky / lint-staged (npm) Ja Ja Automatisch über npm install
Server-seitig (pre-receive) Ja, im Server-Setup Nein Zugriff auf Git-Server erforderlich

In der Praxis ergänzen sich Client- und Server-Hooks: Client-Hooks wie pre-commit geben sofortiges Feedback direkt im Editor-Workflow, während serverseitige pre-receive Hooks als letzte, nicht umgehbare Instanz Regeln durchsetzen, die wirklich verbindlich sein müssen. core.hooksPath macht dabei den entscheidenden Unterschied zwischen einem Hook, der nur auf einer einzigen Maschine existiert, und einem Hook, der Teil der Projekthistorie und für jeden Klon sofort verfügbar ist.

Mironsoft

Git-Hooks-Setup, CI/CD-Automatisierung und Entwickler-Schulungen für Magento-Teams

Git Hooks zuverlässig im Team etablieren?

Wir richten versionierte Git Hooks über core.hooksPath oder Husky für euer Team ein, implementieren serverseitige pre-receive Regeln für geschützte Branches und schulen Entwickler im sicheren Umgang mit lokaler Automatisierung.

Hook-Setup & core.hooksPath

Versionierte pre-commit, commit-msg und pre-push Hooks für euer Repository

Server-Side Enforcement

pre-receive Regeln für geschützte Branches und Commit-Konventionen

Entwickler-Schulung

Praxis-Workshop zu Git Hooks, Exit-Codes und CI/CD-Integration

10. Zusammenfassung

Git Hooks lösen ein zentrales Automatisierungsproblem im Entwickleralltag: wiederkehrende Prüfungen wie Linting, Commit-Message-Formate oder Test-Läufe müssen nicht mehr manuell ausgeführt werden, sondern laufen automatisch bei Commit, Push oder Merge. Client-Hooks wie pre-commit, commit-msg und pre-push geben schnelles, lokales Feedback direkt im Terminal, können aber mit --no-verify umgangen werden und fehlen standardmäßig nach jedem frischen Klon, weil .git/hooks nie Teil des versionierten Projektinhalts ist.

core.hooksPath schließt genau diese Lücke, indem es Git anweist, Hooks aus einem versionierten Projektverzeichnis wie .githooks/ zu laden, statt aus dem nicht versionierten .git/hooks. Für wirklich verbindliche Regeln, die kein Entwickler umgehen können soll, bleibt ein serverseitiger pre-receive Hook auf dem Git-Server die einzige zuverlässige Instanz. Wer Client-Hooks für schnelles Feedback und Server-Hooks für verbindliche Durchsetzung kombiniert, deckt beide Ebenen der Automatisierung sauber ab, unabhängig davon, ob Tools wie Husky oder ein manuell gepflegtes core.hooksPath Setup zum Einsatz kommen.

Git Hooks Grundlagen, Das Wichtigste auf einen Blick

Client vs. Server

Client-Hooks laufen lokal und sind umgehbar, Server-Hooks wie pre-receive laufen auf dem Git-Server und sind verbindlich.

Exit-Codes zählen

Exit-Code 0 erlaubt die Operation, jeder andere Wert bricht sie bei den meisten Hooks vollständig ab.

Nicht versioniert

.git/hooks wird beim Klonen nie mitkopiert, weil .git selbst kein versionierter Projektinhalt ist.

core.hooksPath als Fix

Zeigt Git auf ein versioniertes Verzeichnis wie .githooks/, damit Hooks mit dem Repository mitreisen.

11. FAQ: Git Hooks Grundlagen

1Was ist ein Git Hook genau?
Ein ausführbares Skript, das Git automatisch bei Ereignissen wie Commit, Push oder Merge startet. Liegt als Datei mit festem Namen wie pre-commit im Hooks-Verzeichnis, ohne zusätzliche Registrierung.
2Wo genau liegen Git Hooks im Dateisystem?
Standardmäßig in .git/hooks jedes Repositories. Mit core.hooksPath lässt sich dieser Ort auf ein versioniertes Verzeichnis wie .githooks/ umleiten.
3Warum werden Git Hooks nicht automatisch mit geklont?
Weil .git interner Metadaten-Speicher ist und beim Klonen neu erzeugt statt kopiert wird. Ein automatisch aktives, geklontes Skript wäre zudem ein Sicherheitsrisiko.
4Was unterscheidet Client-Hooks von Server-Hooks?
Client-Hooks laufen auf dem Entwicklerrechner und sind mit --no-verify umgehbar. Server-Hooks wie pre-receive laufen auf dem Git-Server und lassen sich vom Client aus nicht umgehen.
5Welche Client-Hooks werden am häufigsten genutzt?
pre-commit für Linting, commit-msg für Nachrichtenformate, pre-push für Tests vor dem Push, post-checkout und post-merge für Aufräum- und Setup-Aufgaben.
6Wie kann ich einen pre-commit Hook bewusst umgehen?
Mit dem Flag --no-verify, zum Beispiel git commit --no-verify. Funktioniert bei allen Client-Hooks, aber nicht bei serverseitigen Hooks wie pre-receive.
7Was passiert, wenn ein Hook mit Exit-Code 1 endet?
Git bricht die zugehörige Operation vollständig ab. post-commit, post-checkout und post-merge können die Operation nicht mehr stoppen, da sie erst danach laufen.
8Was genau macht core.hooksPath?
Weist Git an, Hooks aus einem frei wählbaren Verzeichnis statt aus .git/hooks zu laden. Dieses Verzeichnis kann Teil des Repositories sein und wird dadurch versioniert.
9Wofür wird ein pre-receive Hook typischerweise genutzt?
Um Force-Pushes auf geschützte Branches zu verhindern, Commit-Konventionen serverseitig zu erzwingen oder signierte Commits zu verlangen, verbindlich und ohne Umgehungsmöglichkeit.
10Sind Tools wie Husky eine Alternative zu core.hooksPath?
Ja, Husky konfiguriert core.hooksPath automatisch über ein npm-Skript, besonders verbreitet in Node.js- und Frontend-lastigen Projekten. Details dazu in einem eigenen Folgeartikel.