Vibecoding Pro Review anfragen

Alle Artikel

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:

  • PreToolUse läuft vor einem Tool-Call und kann ihn blockieren. Das ist dein Türsteher.
  • PostToolUse läuft nach einem erfolgreichen Tool-Call. Gut für Formatter und Linter, aber er kann nichts mehr rückgängig machen.
  • Stop feuert, 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 stattdessen StopFailure.
  • SessionStart und UserPromptSubmit kö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 PreToolUse ist 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 deny von Hook A verhindert nicht, dass Hook B seine Side Effects ausführt. Bei mehreren PreToolUse-Entscheidungen gewinnt die restriktivste.
  • Wenn mehrere Hooks per updatedInput denselben 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:

  1. Ein PostToolUse-Formatter für Edit|Write.
  2. Ein PreToolUse-Guard mit "onFailure": "block" für geschützte Pfade und Befehle.
  3. Ein Stop-Gate mit stop_hook_active-Check.
  4. 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.

Weiterlesen

Weitere Artikel

  1. hooks settings.json Permissions: allow, deny und ask schneiden
  2. subagents Claude Code Subagents: Kontext-Isolation und wann sie lohnen
Kontakt · Warteliste

Setup-Review anfragen

Das Agent-Setup-Review startet gerade. Sag kurz, wo dein Setup steht und was du lösen willst. Wir melden uns, sobald es losgeht, und vorher nur, wenn wir Rückfragen haben.

  1. Anfrage mit Rolle, Tool und größter Baustelle
  2. Rückfrage nur, wenn etwas unklar ist
  3. Nachricht, sobald das Review startet

Du willst ein ganzes Team schulen? Dann ist vibecodingtraining.de die richtige Adresse.

Kein Newsletter, nur eine Nachricht, wenn Review oder Repo starten. Keine Weitergabe. Keine Secrets ins Formular.

Wir nutzen deine Angaben nur, um deine Anfrage zu beantworten. Details in der Datenschutzerklärung.