Git-Hooks zur Automatisierung der Entwicklung
Wie du mit Git-Hooks Quality-Checks, Commit-Message-Validierung, Pre-Push-Tests und Branch-Naming automatisierst – mit Husky und eigenen Skripten.

Git-Hooks sind Scripts, die automatisch an bestimmten Punkten im Git-Workflow laufen – vor einem Commit, vor einem Push, nach einem Merge. Sie sind die Durchsetzungsebene zwischen „wir haben abgesprochen, unseren Code zu linten“ und „unser Code wird tatsächlich gelintet“. Ohne Hooks hängen Qualitätsprüfungen von der Disziplin jedes Entwicklers ab. Mit Hooks laufen sie automatisch.
Die wertvollsten Hooks sind pre-commit (Code vor dem Committen validieren), commit-msg (Commit-Message-Format erzwingen) und pre-push (Tests vor dem Pushen ausführen).
Hooks mit Husky einrichten
Git-Hooks liegen in .git/hooks/, das nicht versioniert wird. Das heißt, du kannst Hooks nicht über das Repository teilen. Husky löst das, indem es Hooks in ein .husky/-Verzeichnis legt, das committet wird.
# Install Husky
npm install --save-dev husky
# Initialize Husky (creates .husky/ directory)
npx husky init
# This creates .husky/pre-commit with a default script
# and adds "prepare": "husky" to package.json// package.json
{
"scripts": {
"prepare": "husky",
"lint": "eslint . --ext .ts,.tsx",
"format:check": "prettier --check .",
"typecheck": "tsc --noEmit",
"test": "vitest run"
},
"devDependencies": {
"husky": "^9.0.0",
"lint-staged": "^15.0.0"
}
}Pre-Commit-Hook: gestagte Dateien linten
Der Pre-Commit-Hook läuft vor jedem Commit. Der entscheidende Optimierungsschritt ist lint-staged: statt die gesamte Codebasis zu linten, werden nur die Dateien gelintet, die committet werden.
# .husky/pre-commit
npx lint-staged// lint-staged.config.ts
const config = {
// TypeScript/JavaScript files: lint and format
'*.{ts,tsx}': [
'eslint --fix --max-warnings 0',
'prettier --write',
],
// CSS/SCSS files: format only
'*.{css,scss}': [
'prettier --write',
],
// JSON/YAML files: format
'*.{json,yaml,yml}': [
'prettier --write',
],
// Markdown files: format
'*.md': [
'prettier --write',
],
};
export default config;# ❌ Linting the entire codebase on every commit
# .husky/pre-commit
npm run lint # Takes 30 seconds on a large codebase
npm run format:check # Another 15 seconds
npm run typecheck # Another 20 seconds
# Developers disable the hook because it's too slow
# ✅ Only check the files being committed
# .husky/pre-commit
npx lint-staged # Takes 2-3 seconds — only changed files
# Fast enough that developers never disable itCommit-Message-Hook: Conventional Commits erzwingen
Der commit-msg-Hook validiert das Format der Commit-Message. Conventional Commits (feat:, fix:, chore: usw.) ermöglichen automatische Changelog-Generierung und semantische Versionierung.
# .husky/commit-msg
npx --no -- commitlint --edit $1// commitlint.config.ts
const config = {
extends: ['@commitlint/config-conventional'],
rules: {
// Type must be one of these
'type-enum': [
2, // Error level
'always',
[
'feat', // New feature
'fix', // Bug fix
'docs', // Documentation only
'style', // Formatting (no code change)
'refactor', // Code change that neither fixes nor adds
'perf', // Performance improvement
'test', // Adding or updating tests
'chore', // Build process or auxiliary tools
'ci', // CI configuration
'revert', // Revert a previous commit
],
],
// Subject must be lowercase
'subject-case': [2, 'always', 'lower-case'],
// Subject must not be empty
'subject-empty': [2, 'never'],
// Subject must not end with period
'subject-full-stop': [2, 'never', '.'],
// Max line length for the header
'header-max-length': [2, 'always', 100],
},
};
export default config;# ❌ Bad commit messages that commit-msg hook rejects
git commit -m "fixed stuff" # No type prefix
git commit -m "feat: Updated the login." # Period at end, uppercase
git commit -m "wip" # No type, not descriptive
# ✅ Good commit messages that pass validation
git commit -m "fix: resolve race condition in payment webhook handler"
git commit -m "feat: add email notification preferences to user settings"
git commit -m "refactor: extract validation logic into shared utility"
git commit -m "docs: add API authentication examples to README"Pre-Push-Hook: Tests vor dem Pushen ausführen
Der Pre-Push-Hook läuft, bevor Code zum Remote gepusht wird. Hier gehören die schwereren Prüfungen, die für Pre-Commit zu langsam wären – Type-Checking und Test-Suites.
# .husky/pre-push
echo "Running type check..."
npx tsc --noEmit
echo "Running tests..."
npx vitest run --reporter=dot
echo "All checks passed. Pushing..."// For monorepos: only run tests for packages that changed
// .husky/pre-push (advanced)
import { execSync } from 'child_process';
function getChangedPackages(): string[] {
// Compare local HEAD with remote
const diff = execSync(
'git diff --name-only @{upstream}...HEAD',
{ encoding: 'utf-8' }
);
const changedFiles = diff.trim().split('\n');
const packages = new Set<string>();
for (const file of changedFiles) {
// Extract package name from path like "packages/auth/src/..."
const match = file.match(/^packages\/([^/]+)\//);
if (match) {
packages.add(match[1]);
}
}
return [...packages];
}
const changed = getChangedPackages();
if (changed.length > 0) {
console.log(`Testing changed packages: ${changed.join(', ')}`);
for (const pkg of changed) {
execSync(`npm run test --workspace=packages/${pkg}`, { stdio: 'inherit' });
}
}Eigene Hooks: Branch-Naming
Über die Standardprüfungen hinaus kannst du Team-Konventionen wie Branch-Naming-Muster durchsetzen.
#!/bin/bash
# .husky/pre-push — branch naming validation
BRANCH=$(git rev-parse --abbrev-ref HEAD)
# Allowed patterns: feat/*, fix/*, chore/*, hotfix/*, release/*
PATTERN="^(feat|fix|chore|hotfix|release|docs|test)\/[a-z0-9._-]+$"
if [[ "$BRANCH" == "main" || "$BRANCH" == "develop" ]]; then
exit 0 # Main branches are always allowed
fi
if [[ ! "$BRANCH" =~ $PATTERN ]]; then
echo "❌ Branch name '$BRANCH' does not match the required pattern."
echo " Use: feat/description, fix/description, chore/description"
echo " Example: feat/add-user-notifications"
exit 1
fi// ❌ Without branch naming enforcement
// Branches in the repo: wip, johns-branch, test123, fix, asdf
// Nobody knows what these branches are for
// ✅ With branch naming enforcement
// Branches: feat/user-notifications, fix/payment-race-condition, chore/update-deps
// Every branch name tells you what it contains and why it existsHooks umgehen (wenn nötig)
Manchmal musst du Hooks überspringen – bei einem Hotfix um 2 Uhr nachts oder beim Committen generierter Dateien. Git bietet Notausgänge, aber die sollten selten sein.
# Skip pre-commit and commit-msg hooks
git commit --no-verify -m "hotfix: emergency patch for production outage"
# Skip pre-push hooks
git push --no-verify
# ⚠️ IMPORTANT: Hooks are a safety net, not a prison.
# If you're using --no-verify regularly, your hooks are too slow or too strict.
# Fix the hooks instead of bypassing them.Die wichtigsten Punkte
- Nutze
lint-stagedim Pre-Commit-Hook — linte nur geänderte Dateien, damit der Hook schnell genug bleibt, dass ihn niemand deaktiviert - Erzwinge Conventional Commits — strukturierte Commit-Messages ermöglichen automatische Changelogs und semantische Versionierung
- Führe Tests im Pre-Push-Hook aus, nicht im Pre-Commit-Hook — schwere Prüfungen gehören vor den Push, nicht vor jeden Commit
- Husky macht Hooks teilbar —
.husky/wird ins Repository committet, sodass jeder Entwickler dieselben Hooks bekommt - Erzwinge Branch-Naming — konsistente Muster machen den Zweck eines Branches sofort erkennbar
- Halte Hooks schnell — dauert ein Hook länger als 5 Sekunden, umgehen ihn Entwickler; optimiere ihn oder verlagere ihn in die CI


