Claude Code soll keinen Müll committen? Bau ihm einen Hook
Anweisungen wie „bitte vorm Commit den Type-Check laufen lassen“ funktionieren manchmal, manchmal nicht. Ein Hook ist nicht verhandelbar — der Type-Check passiert oder der Commit passiert nicht. Drei Hooks zum Anfangen, ohne sich in Husky-Tutorials zu verlaufen.

Du arbeitest mit Claude Code an einem Feature, alles passt, am Ende der Session sagst du „commit das und push". Drei Stunden später schaust du in die CI-Anzeige und siehst rot — ein Type-Fehler, den tsc --noEmit lokal in zwei Sekunden gefunden hätte. Dabei steht in deiner AGENTS.md ausdrücklich „bitte vorm Commit den Type-Check laufen lassen", und Claude hat sich daran in den letzten Wochen auch zuverlässig gehalten. Heute nicht.
Das ist genau die Stelle, an der Memory und AGENTS.md allein an ihre Grenze stoßen. Höfliche Anweisungen funktionieren in achtzig Prozent der Fälle wunderbar. Aber wenn der Agent gerade in einer längeren Sequenz steckt und das Feature zu Ende bringen will, fallen genau diese „bitte mach das vorher noch"-Anweisungen schon mal hinten runter — meistens dann, wenn du sie am dringendsten gebraucht hättest.
Was du dafür brauchst, ist nicht weiter Dokumentation, sondern Durchsetzung. Ein Hook ist genau das: ein kleines Shell-Kommando, das sich automatisch zwischen Claude und das eigentliche Tool schiebt und im Zweifelsfall sagt „nein, der Commit passiert jetzt nicht, weil der Type-Check rot ist". Im letzten Teil dieser Series ging's darum, wie Claude sich Wissen merkt. Heute geht's um den aktiven Gegenpart — wie du Verhalten erzwingst, statt nur darauf zu hoffen.
Was ein Hook eigentlich ist
Ein Hook in Claude Code ist nichts Magisches. Es ist ein Shell-Kommando, das an einer bestimmten Stelle automatisch ausgeführt wird — bevor ein Tool zum Einsatz kommt, nachdem ein Edit gespeichert wurde, beim Start einer Session. Das Kommando bekommt vom Agent ein JSON-Objekt auf stdin (mit tool_name, tool_input etc.) und entscheidet via Exit-Code, was passiert: 0 lässt durch, 2 blockt die Aktion hart (Claude bekommt die stderr-Meldung als Feedback), alles andere wird nur geloggt.
Das war's. Keine Plugin-API, keine Custom-DSL. Alles, was du in einer Bash-Zeile machen kannst, kannst du als Hook hinhängen — jq zum Parsen des stdin-JSONs vorausgesetzt (brew install jq einmalig, falls noch nicht da).
Wo du das einrichtest: in .claude/settings.json (im Repo, alle im Team haben das) oder ~/.claude/settings.json (nur du). Beispiel — der Type-Check-vorm-Commit-Hook von oben, sehr simpel:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{
"type": "command",
"command": "cmd=$(jq -r '.tool_input.command // empty'); case \"$cmd\" in *'git commit'*) npm run typecheck || exit 2 ;; esac"
}]
}
]
}
}
Was passiert: Jedes Mal bevor Claude ein Bash-Kommando ausführt, läuft dieser Check. jq zieht das geplante Kommando aus dem stdin-JSON. Wenn da „git commit" drin steckt, läuft npm run typecheck. Schlägt der fehl, beendet der Hook mit exit 2 — der Commit wird blockiert, Claude bekommt die Fehlermeldung und probiert es erst wieder, nachdem die Type-Fehler gefixt sind.
Das ist nicht mehr „bitte mach das vorher noch", das ist „passiert oder du kommst nicht weiter".
Drei Hooks zum Anfangen
Wenn du noch keine Hooks hast, fang mit diesen drei an. Alle drei haben ein hohes Schutz-zu-Reibungs-Verhältnis und nerven niemanden.
1. Type-Check vor dem Commit (das Beispiel oben) — fängt typische TS/JS-Fehler, bevor sie im CI auftauchen. Wenn dein Projekt kein TypeScript hat, ersetz npm run typecheck durch npm run lint oder npm test. Hauptpunkt: irgendetwas, was schnell läuft (unter 10 Sekunden) und Probleme aufzeigt, bevor sie committed werden.
2. Sperre für --no-verify-Bypass — wenn du Husky oder andere Pre-Commit-Hooks im Projekt hast, gibt's gelegentlich die Versuchung, sie mit git commit --no-verify zu umgehen. Auch Claude greift unter Druck schon mal zu diesem Trick, weil der Hook gerade nervt und das Feature endlich raus soll. Der zugehörige Gegenhook:
{
"matcher": "Bash",
"hooks": [{
"type": "command",
"command": "cmd=$(jq -r '.tool_input.command // empty'); case \"$cmd\" in *'--no-verify'*) echo 'no-verify ist gesperrt — Pre-Commit-Hooks haben Gründe' >&2; exit 2 ;; esac"
}]
}
Ein Pre-Commit-Hook ist da, weil jemand irgendwann mal einen guten Grund hatte. Wenn er rot wird, ist das Signal „hier stimmt was nicht" — nicht „bitte überspringen". Diese Sperre verhindert, dass die Versuchung überhaupt erst Realität wird.
3. Quick-Lint nach jedem Edit — anders als die ersten beiden ist das ein PostToolUse-Hook, also nach (nicht vor) der Aktion. Was passiert: nach jedem Edit/Write läuft eslint --fix automatisch auf der geänderten Datei.
{
"matcher": "Edit|Write",
"hooks": [{
"type": "command",
"command": "file=$(jq -r '.tool_input.file_path // empty'); case \"$file\" in *.ts|*.tsx|*.js|*.jsx) npx eslint --fix \"$file\" 2>/dev/null || true ;; esac"
}]
}
Das || true am Ende ist wichtig: Lint-Fehler sollen den Edit nicht rückgängig machen (das wäre destruktiv und nervig), nur fixable Sachen werden automatisch gefixt. Alles, was sich nicht automatisch reparieren lässt, taucht beim nächsten Tool-Call ohnehin im normalen Output auf.
Diese drei zusammen — Type-Check, no-verify-Sperre, Auto-Lint — fangen vermutlich 90 % der „eigentlich hätte das nicht passieren dürfen"-Fälle ab.
Was Hooks nicht sind
Drei Missverständnisse, die in Gesprächen mit anderen Teams immer wieder auftauchen:
„Hooks ersetzen Code-Review." Tun sie nicht — sie verifizieren formale Kriterien wie einen grünen Type-Check, einen sauberen Linter-Lauf oder ein fehlendes Secret-Pattern im Diff. Ob die Architektur sinnvoll ist oder die Lösung das eigentliche Problem trifft, kann ein Hook nicht beurteilen. Das Pre-Merge-Review bleibt deine Aufgabe.
„Hooks ersetzen CI." Auch nicht. Was lokal in einem Pre-Commit-Hook läuft, muss schnell sein — typischerweise unter zehn Sekunden. Alles, was länger dauert (Test-Suite, Browser-E2E, Performance-Benches), gehört in die CI. Hooks sind die schnelle Filterstufe direkt am Editor, die CI ist die gründliche Prüfung dahinter.
„Hooks sind eine Sandbox." Sind sie nicht — sie sind eine Komfortschicht, kein Security-Mechanismus. Ein böswilliger Agent (was Claude nicht ist, aber für die Argumentation) könnte die settings.json einfach selbst editieren und seine eigenen Hooks ausschalten. Wer eine echte Sandbox braucht, kommt um Container-Isolation oder restriktive Filesystem-Permissions nicht herum.
Wann Hooks Schmerzen machen
Drei Stellen, wo wir selbst Hooks ausprobiert und wieder rausgenommen haben:
1. Hooks auf Read-Operationen — die Idee, vor jedem Datei-Read zu prüfen, ob die Datei in einem erlaubten Pfad liegt, klingt vernünftig. In der Praxis ist's zu viel Reibung: Reads sind harmlos, der Hook hat bei uns 200 Millisekunden pro Read gekostet, und in Sessions mit fünfzig oder mehr Reads summierte sich das auf rund zehn Sekunden verlorene Zeit.
2. Voll-Linter im PostToolUse (statt nur die geänderte Datei) — der Versuch, Prettier oder ESLint nach jedem Edit auf das ganze Repo zu jagen, ist schmerzhaft. Auf größeren Codebases dauert das schnell dreißig Sekunden, und du wartest bei jeder kleinen Änderung. Entweder du beschränkst den Lint-Lauf auf die geänderte Datei oder du lässt ihn ganz weg.
3. Notification-Hooks bei jedem Stop — am Anfang charmant, am Ende laut. Wenn der Hook bei jeder fertigen Claude-Antwort eine macOS-Notification feuert, kriegst du in einer aktiven Session schnell fünfzig Pings pro Stunde. Solche Notifications gehören in deine persönliche settings.local.json (lokal, gitignored) und sehr selektiv — nur bei langen Background-Tasks, nicht bei jedem Turn.
Faustregel: ein Hook braucht ein klares Schutz-zu-Reibungs-Verhältnis. Wenn er mehr Zeit kostet, als er Probleme einfängt, raus.
Eine kleine Heuristik für die nächste Hook-Entscheidung
Bevor du einen Hook einbaust, frag dich:
Wie oft greift der Hook? Bei jedem Tool-Call (50× pro Session)? Einmal pro Session? Das verändert die Reibungsbilanz dramatisch.
Wie lange läuft er? Über 500 Millisekunden in einem Pre-Hook ist verdächtig. Das summiert sich.
Lässt er sich umgehen? Wenn die Antwort „ja, einfach den Hook löschen" lautet und der Bypass echten Schaden anrichten würde, ist ein Hook das falsche Werkzeug — dann brauchst du ein Architektur-Pattern oder echte Permissions.
Würde eine AGENTS.md-Anweisung reichen? Bei Stilfragen wie „bitte schreibe Funktionsnamen in camelCase" reicht die Anweisung in der Regel völlig aus. Bei harten Constraints wie „kein Commit ohne grünen Type-Check" gehört es in einen Hook.
Drei bis fünf Hooks pro Repo sind meistens das Optimum. Wenn dein Settings-File über zehn Einträge wächst, ist es Zeit für einen Aufräum-Pass.
Was du diese Woche machen kannst
Wenn du Claude Code nutzt und noch keinen einzigen Hook hast: nimm dir 10 Minuten und füg den Type-Check-Pre-Commit-Hook hinzu. Das ist die niedrigste Hürde, der höchste Schutzeffekt. Auch wenn du in den letzten zwei Wochen keinen Type-Fehler im CI hattest — der Hook kostet dich nichts, und beim ersten Mal, wo er greift, hast du die Investition raus.
Wenn du schon einen oder zwei Hooks hast, schau einmal kritisch drauf — laufen die wirklich in unter 500ms? Greifen sie in echten Fällen, oder sind sie immer grün und nur Dekoration? Hooks, die noch nie ein Problem gefangen haben, kannst du oft ersatzlos streichen.
Im nächsten Teil
Nach Memory (dokumentiert) und Hooks (erzwingt) kommt der dritte Mechanismus: Slash-Commands — wiederkehrende Workflows als reproduzierbare Befehle. Wenn du dich dabei ertappst, denselben langen Prompt mehrmals pro Woche zu tippen — „mach mir einen neuen Newsroom-Beitrag mit Frontmatter, Tags, Bild-Prompt" — dann ist das Material für einen eigenen Skill. Im nächsten Teil zeige ich, wie du das in 15 Minuten baust und wann du's lieber sein lassen solltest.
Falls du gerade neu in die Series einsteigst — der erste Teil zum Account-Setup liegt unter „So nutzt du mehrere Claude Code Accounts gleichzeitig", und Teil 2 zum Memory-Setup unter „Claude Code merkt sich nichts? Doch — du musst ihm nur sagen wo".