Claude Code Hooks: deterministische Regeln für Agenten
Wie Claude Code Hooks funktionieren: Events, Exit-Codes, JSON-Output, onFailure block und typische Fallen. Für Regeln, die der Agent nicht vergessen darf.
Du schreibst in die CLAUDE.md „nach jedem Edit Prettier laufen lassen” und „niemals .env anfassen”. Meistens hält sich der Agent daran. Meistens. Das Problem: Alles, was in CLAUDE.md oder im Prompt steht, ist eine Bitte an ein Modell. Wenn eine Regel immer gelten soll, gehört sie nicht in den Kontext, sondern in einen Hook.
Hooks sind Shell-Befehle (oder HTTP-Calls, MCP-Tools, Prompt- und Agent-Checks), die Claude Code an festen Punkten im Lebenszyklus selbst ausführt. Ob sie laufen, entscheidet nicht das Modell. Das ist der ganze Punkt. Stand dieses Artikels ist die offizielle Doku im Oktober 2026, Claude Code v2.1.295.
Die Events, die du wirklich brauchst
Die Liste der Hook-Events ist inzwischen lang (von SessionStart über PreCompact bis WorktreeCreate). Für Guardrails und Qualität reichen in der Praxis wenige:
PreToolUseläuft vor einem Tool-Call und kann ihn blockieren. Das ist dein Türsteher.PostToolUseläuft nach einem erfolgreichen Tool-Call. Gut für Formatter und Linter, aber er kann nichts mehr rückgängig machen.Stopfeuert, wenn Claude mit einer Antwort fertig ist, nicht erst am Ende der Aufgabe. Bei einem Abbruch durch dich feuert er nicht, bei API-Fehlern kommt stattdessenStopFailure.SessionStartundUserPromptSubmitkönnen Kontext einspeisen, weil ihr stdout bei Exit 0 als Text an Claude geht.
Konfiguriert wird im hooks-Block einer Settings-Datei. Ein matcher filtert nach Tool-Namen, Edit|Write trifft beide Datei-Tools:
// .claude/settings.json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write" }
]
}
]
}
}
Wo der Block liegt, bestimmt den Scope: ~/.claude/settings.json gilt für alle deine Projekte, .claude/settings.json wird mit dem Repo geteilt, .claude/settings.local.json bleibt lokal. Dazu kommen Managed Settings, Plugins sowie die Frontmatter von Skills und Subagents. Mit /hooks siehst du alles nach Event gruppiert.
Exit-Codes: der Teil, den alle falsch machen
Der Hook bekommt das Event als JSON auf stdin (session_id, cwd, tool_name, tool_input usw.) und antwortet über Exit-Code und Ausgabe:
- Exit 0 heißt nur „kein Einwand”. Bei
PreToolUseist das ausdrücklich keine Freigabe: Der normale Permission-Flow läuft trotzdem. - Exit 2 blockiert. Was du auf stderr schreibst, landet je nach Event als Feedback bei Claude, sodass der Agent seinen Ansatz anpassen kann.
- Jeder andere Exit-Code ist ein nicht-blockierender Fehler. Die Aktion läuft weiter, du siehst nur einen
hook error-Hinweis im Transcript.
Ein minimaler Guard für Edits an sensiblen Dateien:
#!/bin/bash
# .claude/hooks/protect-files.sh (chmod +x nicht vergessen)
path=$(jq -r '.tool_input.file_path // empty')
case "$path" in
*.env|*.env.*|*/.git/*|*package-lock.json)
echo "Geschützte Datei: $path. Bitte nicht direkt editieren." >&2
exit 2 ;;
esac
exit 0
Registriert wird er als PreToolUse mit Matcher Edit|Write und dem Befehl "$CLAUDE_PROJECT_DIR"/.claude/hooks/protect-files.sh, damit der Pfad auch aus Unterverzeichnissen stimmt.
Wenn dir Exit-Codes zu grob sind: Exit 0 plus JSON auf stdout. Für PreToolUse gibt es hookSpecificOutput.permissionDecision mit deny, ask oder allow und einen permissionDecisionReason, den Claude als Begründung bekommt. Die Doku rät, pro Hook nur einen der beiden Wege zu nutzen.
Die Falle: ein kaputter Hook lässt alles durch
Hier liegt der wichtigste Fallstrick für Policy-Hooks. Wenn ein Hook nicht startet, abstürzt, ungültiges JSON ausgibt oder in den Timeout läuft, führt Claude Code die Aktion standardmäßig trotzdem aus. Ein vertippter Pfad in der settings.json bedeutet also: Dein Guard läuft nie, und du merkst es nur am Hinweis im Transcript.
Seit v2.1.295 gibt es dafür onFailure. Mit "onFailure": "block" an einem command- oder http-Hook wird die Aktion blockiert, wenn der Hook selbst scheitert. Standard bleibt "continue". Für alles, was Sicherheit oder Compliance absichert, solltest du block setzen und den Hook einmal bewusst mit fehlendem Skript testen.
Weitere Details, die man kennen sollte:
- Alle passenden Hooks eines Events laufen parallel. Ein
denyvon Hook A verhindert nicht, dass Hook B seine Side Effects ausführt. Bei mehrerenPreToolUse-Entscheidungen gewinnt die restriktivste. - Wenn mehrere Hooks per
updatedInputdenselben Tool-Input umschreiben, gewinnt der zuletzt fertige. Das ist nicht deterministisch, also lass das. - Standard-Timeout für
command-Hooks sind 10 Minuten,SessionEnd-Hooks teilen sich ein sehr knappes Budget von 1,5 Sekunden.
Hooks und Permissions spielen zusammen, nicht gegeneinander
Ein PreToolUse-Hook mit deny greift in jedem Permission-Mode, auch bei bypassPermissions und --dangerously-skip-permissions. Umgekehrt hebelt ein allow aus dem Hook keine Deny-Regeln aus der settings.json aus. Hooks können also verschärfen, aber nicht über die Regeln hinaus lockern. Wie du die Regeln selbst schneidest, steht im Artikel zu settings.json Permissions.
Ein Muster aus der Doku, das sich lohnt: "Bash" komplett erlauben und die wenigen gefährlichen Befehle per PreToolUse-Hook blocken, der Exit 2 liefert. Der blockierende Hook greift, bevor die Allow-Regeln ausgewertet werden.
Stop-Hooks als Quality Gate
Ein Stop-Hook, der blockiert, schickt Claude zurück an die Arbeit, zum Beispiel weil Tests noch rot sind. Für Urteile statt fester Regeln gibt es type: "prompt" (ein einzelner Modell-Call, der {"ok": false, "reason": ...} liefert) und den noch experimentellen type: "agent", der selbst Tools nutzen und etwa die Test-Suite laufen lassen darf.
Wichtig: Claude Code übergeht einen Stop-Hook, nachdem er achtmal in Folge ohne Tool-Call dazwischen blockiert hat. Dein Skript sollte stop_hook_active aus dem Input lesen und früh mit Exit 0 aussteigen, wenn es bereits eine Fortsetzung ausgelöst hat. Sonst baust du eine Schleife, die am Cap endet.
Hooks lassen sich übrigens auch in der Frontmatter eines Subagents definieren und gelten dann nur, solange dieser läuft. Mehr dazu im Artikel über Claude Code Subagents.
Was das für deinen Workflow bedeutet
Teil deine Regeln in zwei Gruppen. Alles, was Urteil braucht (Architektur, Stil, Prioritäten), bleibt in CLAUDE.md. Alles, was immer gelten muss (Formatter, geschützte Pfade, gefährliche Befehle, Tests vor dem Ende), wandert in Hooks. Konkret:
- Ein
PostToolUse-Formatter fürEdit|Write. - Ein
PreToolUse-Guard mit"onFailure": "block"für geschützte Pfade und Befehle. - Ein
Stop-Gate mitstop_hook_active-Check. - Projekt-Hooks in
.claude/settings.json, damit das ganze Repo dieselben Regeln hat.
Prüf bestehende Hooks auf stille Fehler. Ein Guard, der seit Wochen mit Exit 127 scheitert, ist schlimmer als keiner, weil er Sicherheit vortäuscht. Eine Übersicht der Dateien, die zu einem sauberen Setup gehören, findest du auf der Setup-Seite.
Du willst wissen, ob die Hooks und Permissions in deinem Repo wirklich greifen oder nur so aussehen? Fordere ein Agent-Setup-Review an, das deine settings.json, Hook-Skripte und CLAUDE.md schriftlich durchgeht.