Pre-Commit-Hooks für Code-Qualität einrichten
git
HEAD
Git · Pre-Commit-Hooks · Code-Qualität · Linting
Pre-Commit-Hooks für Code-Qualität einrichten
Schnelle Prüfungen, bevor der Commit überhaupt entsteht

Ein pre-commit Hook läuft, bevor Git den Commit tatsächlich erstellt, und kann ihn bei Regelverstößen vollständig verhindern. Dieser Artikel zeigt, welche Prüfungen wie Linting, Formatierung und schnelle Unit-Tests dort sinnvoll sind, warum Geschwindigkeit entscheidend ist, und wie sich Hand-geschriebene Skripte gegenüber dem pre-commit Framework schlagen.

11 Min. Lesezeit Linting · Formatierung · Unit-Tests pre-commit Framework · pre-commit.com

1. Warum der pre-commit Hook der erste Kontrollpunkt ist

Ein pre-commit Hook läuft, nachdem Dateien mit git add gestaged wurden, aber bevor Git den eigentlichen Commit-Editor öffnet und das Commit-Objekt erzeugt. Genau dieser Zeitpunkt macht ihn zum frühesten sinnvollen Kontrollpunkt im gesamten Entwickler-Workflow: Ein Fehler wird erkannt, bevor er überhaupt Teil der lokalen Historie wird, nicht erst beim Push oder in der CI-Pipeline Minuten später. Bricht der Hook mit einem Nicht-Null-Exit-Code ab, verhindert Git den Commit vollständig, und der Entwickler landet zurück im Terminal mit einer klaren Fehlermeldung.

Der Vorteil gegenüber späteren Prüfungen liegt im Kontextwechsel: Ein Entwickler, der noch mitten in der Änderung steckt, kann einen Fehler sofort beheben, ohne erst zu einer alten Änderung zurückspringen zu müssen, die längst aus dem Kopf verschwunden ist. Ein pre-commit Hook ersetzt dabei keine vollständige CI-Pipeline, sondern ergänzt sie um eine schnelle, lokale Vorstufe, die offensichtliche Probleme abfängt, bevor sie überhaupt in die geteilte Historie gelangen.

2. Wie der pre-commit Hook technisch abläuft

Technisch erhält ein pre-commit Hook keine Kommandozeilenargumente, sondern muss selbst über git diff --cached --name-only --diff-filter=ACM ermitteln, welche Dateien tatsächlich gestaged sind. Wichtig ist, ausschließlich den gestagten Inhalt zu prüfen, nicht die Version im Arbeitsverzeichnis, denn bei teilweise gestagten Dateien mit git add -p können beide Versionen voneinander abweichen. Der zuverlässige Weg dafür ist git show :datei.php, das den Inhalt direkt aus dem Staging-Bereich liest.

Nach der Prüfung entscheidet ausschließlich der Exit-Code über das Ergebnis, nicht die Textausgabe. Ein Exit-Code von 0 lässt den Commit zu, jeder andere Wert bricht ihn ab. Aus diesem Grund sollten alle Teilprüfungen ihr Ergebnis in einer Sammelvariablen bündeln, statt beim ersten Fehler sofort abzubrechen, damit der Entwickler in einem Durchlauf alle Probleme sieht, statt sie nacheinander einzeln zu entdecken.


#!/usr/bin/env bash
# pre-commit hook: determine staged files and read their staged content
set -euo pipefail

# Only added, copied and modified files, deletions are irrelevant to lint
staged_files=$(git diff --cached --name-only --diff-filter=ACM)

if [ -z "$staged_files" ]; then
  echo "No staged files, skipping checks."
  exit 0
fi

exit_code=0

for file in $staged_files; do
  case "$file" in
    *.php)
      # Read the staged content directly, not the working-directory version
      if ! git show ":$file" | php -l > /dev/null 2>&1; then
        echo "Syntax error in staged version of: $file"
        exit_code=1
      fi
      ;;
  esac
done

exit $exit_code

3. Linting als Basis-Prüfung im pre-commit Hook

Linting ist die naheliegendste und meist gewinnbringendste Prüfung in einem pre-commit Hook, weil sie rein statisch arbeitet und keine Laufzeitumgebung benötigt. Ein Linter erkennt unbenutzte Variablen, fehlerhafte Syntax, verbotene Konstrukte wie var_dump in produktivem PHP-Code oder Verstöße gegen einen vereinbarten Coding-Standard wie PSR-12. Weil Linting typischerweise nur Millisekunden bis wenige Sekunden pro Datei benötigt, lässt es sich problemlos bei jedem einzelnen Commit ausführen, ohne den Workflow spürbar zu verlangsamen.

Entscheidend ist, den Linter ausschließlich auf die gestagten Dateien anzuwenden, nicht auf das gesamte Repository. Ein vollständiger Lint-Lauf über tausende Dateien bei jedem Commit wäre selbst bei einem schnellen Linter unpraktikabel und würde die eigentliche Idee eines schnellen, lokalen Kontrollpunkts konterkarieren. Tools wie phpcs, eslint oder ruff unterstützen dafür alle eine gezielte Angabe einzelner Dateipfade als Argument.

4. Formatierung automatisch korrigieren statt nur melden

Formatierungsprüfungen unterscheiden sich von Linting durch einen entscheidenden Punkt: Sie lassen sich in den meisten Fällen automatisch beheben, statt nur gemeldet zu werden. Tools wie php-cs-fixer, prettier oder black formatieren den Code direkt um und geben ihn in korrigierter Form zurück, statt dem Entwickler lediglich eine Liste von Abweichungen zu präsentieren, die er von Hand nachbessern müsste. Ein gut konfigurierter pre-commit Hook nutzt diese Fähigkeit, formatiert die gestagten Dateien im Arbeitsverzeichnis neu und fügt die korrigierten Versionen mit git add erneut zum Staging-Bereich hinzu.

Diese Automatik nimmt Entwicklern die mühsame manuelle Formatierungsarbeit vollständig ab und beendet gleichzeitig endlose Diskussionen über Einrückung oder Anführungszeichen in Code-Reviews. Wichtig ist, dass der Hook nach dem automatischen Fix erneut prüft, ob noch Formatierungsfehler übrig sind, die das Tool nicht automatisch lösen konnte, und in diesem Fall den Commit weiterhin ablehnt.


#!/usr/bin/env bash
# pre-commit hook: auto-fix formatting, then re-stage the fixed files
set -euo pipefail

staged_php=$(git diff --cached --name-only --diff-filter=ACM -- '*.php')

if [ -z "$staged_php" ]; then
  exit 0
fi

# Auto-fix formatting in place on the working-directory copies
php-cs-fixer fix --using-cache=no $staged_php

# Re-add the now-formatted files so the fix is part of this commit
git add $staged_php

# Fail only if something remains that the fixer could not resolve
if ! php-cs-fixer fix --dry-run --diff $staged_php > /dev/null 2>&1; then
  echo "Formatting could not be fully auto-fixed, please review manually."
  exit 1
fi

exit 0

5. Schnelle Unit-Tests im pre-commit Hook

Unit-Tests im pre-commit Hook sind sinnvoll, solange sie schnell genug bleiben, um den Commit-Vorgang nicht spürbar zu verzögern. Die praktikable Grenze liegt bei den meisten Teams bei wenigen Sekunden, keinesfalls bei mehreren Minuten. Statt die gesamte Testsuite auszuführen, beschränkt sich ein pre-commit Hook idealerweise auf Tests, die direkt zu den gestagten Dateien gehören, etwa über eine Namenskonvention wie ClassNameTest oder eine explizite Zuordnung in der Testkonfiguration.

Integrationstests, die eine Datenbank, einen externen Dienst oder einen vollständigen Applikations-Bootstrap benötigen, gehören in der Regel nicht in den pre-commit Hook, sondern bleiben Aufgabe der CI-Pipeline nach dem Push. Ein pre-commit Hook, der versucht, die komplette Testsuite abzudecken, wird über kurz oder lang deaktiviert oder mit --no-verify umgangen, weil er den Entwicklungsfluss zu stark unterbricht. Schnelle, gezielte Unit-Tests direkt an der Änderung liefern den besten Kompromiss zwischen Sicherheit und Geschwindigkeit.

6. Geschwindigkeit: warum langsame Hooks Entwickler frustrieren

Ein pre-commit Hook, der spürbar wartet, wird schnell zum größten Widerstand im täglichen Workflow, weil Commits typischerweise sehr häufig erstellt werden, oft mehrfach pro Stunde. Schon ein Hook, der zehn Sekunden benötigt, summiert sich über einen Arbeitstag zu erheblicher Wartezeit, und Entwickler beginnen fast zwangsläufig, nach Wegen zu suchen, ihn zu umgehen, etwa mit git commit --no-verify. Sobald dieser Reflex zur Gewohnheit wird, ist die eigentliche Schutzfunktion des Hooks faktisch außer Kraft gesetzt.

Die wirksamste Maßnahme gegen langsame Hooks ist, ausschließlich die gestagten Dateien statt des gesamten Repositorys zu prüfen, wie bereits bei Linting und Formatierung beschrieben. Zusätzlich hilft ein expliziter Timeout, der einen hängenden Prozess nach wenigen Sekunden abbricht, statt den Entwickler unbegrenzt warten zu lassen. Teure Prüfungen wie ein vollständiger statischer Analyzer über das gesamte Projekt gehören in den pre-push Hook oder die CI-Pipeline, nicht in den pre-commit Hook, der bei jedem einzelnen Commit läuft.


#!/usr/bin/env bash
# pre-commit hook: bound total runtime and only touch staged files
set -euo pipefail

readonly TIMEOUT_SECONDS=8

staged_files=$(git diff --cached --name-only --diff-filter=ACM)

if [ -z "$staged_files" ]; then
  exit 0
fi

# Wrap the whole check in a hard timeout so a hanging tool never blocks a commit
if ! timeout "$TIMEOUT_SECONDS" bash -c '
  files="$1"
  for f in $files; do
    case "$f" in
      *.php) php -l "$f" > /dev/null ;;
    esac
  done
' _ "$staged_files"; then
  echo "pre-commit checks exceeded ${TIMEOUT_SECONDS}s or failed, aborting commit."
  exit 1
fi

exit 0

7. Hand-geschriebene Shell-Skripte: Flexibilität mit Wartungslast

Ein selbst geschriebenes Shell-Skript als pre-commit Hook bietet uneingeschränkte Flexibilität: Jede beliebige Logik lässt sich direkt in Bash, Python oder einer anderen Sprache umsetzen, ohne fremde Abhängigkeiten. Für kleine Teams mit einem einzigen Tool-Stack, etwa ausschließlich PHP, ist ein kompaktes, selbst gepflegtes Skript oft schnell eingerichtet und leicht nachvollziehbar, weil der gesamte Ablauf in einer einzigen, lesbaren Datei steht.

Mit wachsender Zahl an Tools und Sprachen im Projekt wächst allerdings auch die Wartungslast: Jedes neue Tool muss von Hand in das Skript integriert werden, inklusive eigener Fehlerbehandlung, eigener Dateifilterung und eigener Versionsverwaltung der eingesetzten Linter. Läuft ein Team mit unterschiedlichen Betriebssystemen, treten zusätzlich subtile Unterschiede zwischen Bash-Versionen auf macOS und Linux auf. Genau an diesem Punkt lohnt sich der Blick auf ein etabliertes Framework, das diese Wiederholarbeit bereits gelöst hat.

8. Das pre-commit Framework als sprachunabhängiger Multi-Tool-Runner

Das pre-commit Framework, verfügbar über pre-commit.com, löst genau das Wartungsproblem hand-geschriebener Skripte: Es ist selbst sprachunabhängig, verwaltet aber die Umgebungen der einzelnen Tools automatisch, unabhängig davon, ob ein Tool in Python, Go, Rust oder Node.js geschrieben ist. Statt jedes Tool manuell zu installieren und im Skript aufzurufen, deklariert eine YAML-Datei .pre-commit-config.yaml lediglich, welche Hooks aus welchem Repository in welcher Version verwendet werden sollen.

Beim ersten Lauf lädt das Framework die passenden isolierten Umgebungen für jedes Tool automatisch herunter und cached sie, sodass spätere Läufe schnell bleiben. Jedes Team-Mitglied erhält so garantiert dieselbe Tool-Version, ohne dass eine globale Installation auf jedem Rechner nötig wäre. Ergänzend übernimmt das Framework auch das automatische Filtern auf gestagte Dateien, das Parallelisieren mehrerer Hooks und eine einheitliche, gut lesbare Ausgabe, Dinge, die bei einem hand-geschriebenen Skript jeweils selbst implementiert werden müssten.


# .pre-commit-config.yaml, versioned in the repository, shared by the whole team
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
      - id: check-merge-conflict

  - repo: https://github.com/PHP-CS-Fixer/PHP-CS-Fixer
    rev: v3.57.0
    hooks:
      - id: php-cs-fixer
        args: [--config=.php-cs-fixer.php]

  - repo: local
    hooks:
      - id: php-lint
        name: PHP syntax check
        entry: php -l
        language: system
        files: \.php$

# Install once per clone, then it runs automatically on every commit
# pre-commit install

9. Hand-geschriebene Skripte vs. pre-commit Framework im Vergleich

Beide Ansätze lösen dasselbe Grundproblem, unterscheiden sich aber erheblich in Wartbarkeit, Portabilität und Startaufwand. Die folgende Übersicht fasst die wichtigsten Unterschiede zusammen.

Dimension Hand-geschriebenes Skript pre-commit Framework
Sprachunabhängigkeit Muss pro Tool manuell integriert werden Ja, einheitlich über alle Sprachen
Tool-Versionierung Abhängig von lokaler Installation Fest in der YAML-Datei gepinnt
Einrichtungsaufwand Gering bei einem einzigen Tool Einmalige Installation, danach automatisch
Wartung bei neuen Tools Manuelles Einbauen ins Skript nötig Neuer Eintrag in der YAML-Datei genügt
Plattformübergreifend Bash-Unterschiede zwischen macOS/Linux möglich Isolierte Umgebungen pro Plattform
Volle Kontrolle über Logik Uneingeschränkt frei Über local-Hooks weiterhin möglich

Für kleine, homogene Projekte mit einem einzigen Tool-Stack bleibt ein schlankes, hand-geschriebenes Skript eine legitime Wahl. Sobald mehrere Sprachen, mehrere Tools oder ein wachsendes Team im Spiel sind, reduziert das pre-commit Framework die Wartungslast erheblich und stellt sicher, dass jeder Entwickler dieselben Tool-Versionen nutzt, ohne dass jemand die Installation von Hand pflegen muss.

Mironsoft

Git-Workflows, Code-Qualität und CI/CD-Automatisierung für Entwicklerteams

Pre-Commit-Hooks, die Entwickler wirklich nutzen?

Wir richten schnelle, gezielte pre-commit Hooks für euer Team ein, von einem schlanken Shell-Skript bis zum vollständigen pre-commit Framework, mit Linting, Formatierung und schnellen Tests, die den Workflow nicht ausbremsen.

Hook-Einrichtung

pre-commit Framework oder Hand-geschriebene Skripte, passend zu eurem Stack

Linting & Formatierung

Automatische Prüfungen und Auto-Fix direkt vor dem Commit

Performance-Tuning

Hooks so schnell machen, dass niemand sie umgehen will

10. Zusammenfassung

Pre-Commit-Hooks sind der früheste sinnvolle Kontrollpunkt im Git-Workflow, weil sie laufen, bevor der Commit überhaupt entsteht, und ihn bei einem Fehler vollständig verhindern können. Linting, automatische Formatierung und schnelle, gezielte Unit-Tests decken die meisten praxisrelevanten Fälle ab, solange sie sich konsequent auf die gestagten Dateien beschränken. Geschwindigkeit ist dabei keine Nebensache: Ein Hook, der spürbar wartet, wird über kurz oder lang mit --no-verify umgangen und verliert damit seine eigentliche Schutzfunktion.

Hand-geschriebene Shell-Skripte bieten volle Kontrolle und eignen sich gut für kleine, homogene Projekte. Das pre-commit Framework von pre-commit.com reduziert dagegen die Wartungslast erheblich, sobald mehrere Sprachen und Tools im Spiel sind, weil es Umgebungen, Versionierung und Dateifilterung zentral über eine einzige YAML-Datei regelt. Die richtige Wahl hängt weniger vom persönlichen Geschmack ab als von der Größe und Heterogenität des Projekts.

Pre-Commit-Hooks für Code-Qualität, das Wichtigste auf einen Blick

Frühester Kontrollpunkt

Läuft vor der Commit-Erstellung und kann sie bei Regelverstößen vollständig verhindern.

Sinnvolle Prüfungen

Linting, automatische Formatierung mit Re-Add und schnelle, gezielte Unit-Tests an den gestagten Dateien.

Geschwindigkeit zählt

Nur gestagte Dateien prüfen, Timeouts setzen, teure Prüfungen in pre-push oder CI verschieben.

pre-commit Framework

Sprachunabhängiger Multi-Tool-Runner über .pre-commit-config.yaml, löst die Wartungslast hand-geschriebener Skripte.

11. FAQ: Pre-Commit-Hooks für Code-Qualität

1Was macht ein pre-commit Hook genau?
Er läuft nach dem Staging, aber vor der Commit-Erstellung. Ein Nicht-Null-Exit-Code verhindert den Commit vollständig.
2Welche Prüfungen gehören in einen pre-commit Hook?
Linting, automatische Formatierung und schnelle Unit-Tests an gestagten Dateien. Langsame Integrationstests gehören in pre-push oder die CI-Pipeline.
3Warum nur gestagte Dateien prüfen?
Ein vollständiger Repository-Lauf bei jedem Commit wäre unpraktikabel und würde die Idee eines schnellen Kontrollpunkts konterkarieren.
4Wie lange darf ein pre-commit Hook maximal dauern?
Wenige Sekunden. Länger führt fast zwangsläufig zu --no-verify und verliert damit die Schutzfunktion.
5Kann Formatierung automatisch korrigiert werden?
Ja, Tools wie php-cs-fixer formatieren direkt um. Der Hook fügt die korrigierten Dateien erneut mit git add hinzu.
6Was ist das pre-commit Framework von pre-commit.com?
Ein sprachunabhängiger Multi-Tool-Runner, der Hooks per YAML-Datei deklariert und Tool-Umgebungen automatisch verwaltet.
7Wann lohnt sich ein hand-geschriebenes Skript?
Bei kleinen, homogenen Projekten mit einem einzigen Tool-Stack ist ein schlankes eigenes Skript oft schneller eingerichtet.
8Wie prüfe ich den tatsächlich gestagten Inhalt?
Mit git show :datei.php statt die Datei von der Festplatte zu lesen, wichtig bei teilweise gestagten Dateien.
9Kann ich Unit-Tests komplett verlagern?
Nein, nur schnelle, gezielte Tests. Die vollständige Testsuite bleibt Aufgabe der CI-Pipeline nach dem Push.
10Wie installiere ich das Framework im Team?
Die YAML-Datei wird im Repository versioniert, jedes Mitglied führt einmalig pre-commit install aus.