Ein Agent, den ich beaufsichtigte, versuchte einmal, git reset --hard origin/main auf einem Branch mit zwei Stunden nicht committeter Arbeit auszuführen. Er hatte entschieden, dass der Working Tree "unordentlich" war und wollte einen sauberen Neustart. Der einzige Grund, warum ich diese Arbeit noch habe, ist ein PreToolUse-Hook, der jeden git reset --hard blockiert.
Das ist das vollständige Argument für Claude Code hooks. Sie sind der Unterschied zwischen "das Modell verhält sich meistens korrekt" und "das Modell kann diese Aktion buchstäblich nicht ausführen." Prompts sind Vorschläge. Hooks sind Gesetz, weil das Harness sie ausführt, nicht das Modell.
Warum ein Hook immer einen Prompt schlägt
Du kannst "FÜHRE NIE destruktive Git-Befehle aus" in deinen Systemprompt schreiben, komplett in Großbuchstaben mit drei Ausrufezeichen. Auf Opus 4.8 funktioniert das meistens — es folgt Anweisungen genau. Aber "meistens" ist das Problem. Das Verhalten des Modells ist eine Wahrscheinlichkeitsverteilung. Ein Hook ist eine Bedingung. Wenn die Sicherheitsgrenze wirklich zählt — Daten löschen, in Produktion pushen, eine E-Mail senden — willst du die Bedingung, nicht die Verteilung.
Es gibt einen tieferen Grund darin, wie diese Modelle über Werkzeuge nachdenken. Das Modell gibt einen Tool-Call aus; dein Harness entscheidet, was damit zu tun ist. Ein Bash-Werkzeug übergibt dem Harness eine undurchsichtige Befehlszeichenkette. Ein Hook ist der Ort, an dem du diese Zeichenkette öffnest und eine deterministische Entscheidung triffst: erlauben, blockieren oder fragen. Die Absicht des Modells spielt dabei keine Rolle.
Die drei Hooks, die ich wirklich nutze
PreToolUse — der Türsteher. Wird ausgelöst, bevor ein Werkzeug ausgeführt wird. Erhält den Werkzeugnamen und die Eingabe. Gib eine Blockierentscheidung zurück, und der Aufruf findet nie statt. Hier lebt mein git reset --hard-Schutz, plus Blockierungen für rm -rf außerhalb eines Scratch-Verzeichnisses und jedes curl, das in eine Shell geleitet wird.
# PreToolUse: destruktives Git beim Bash-Werkzeug ablehnen
if echo "$TOOL_INPUT" | grep -qE 'git reset --hard|git clean -[a-z]*f|git push --force'; then
echo '{"decision":"block","reason":"Destructive git blocked. Stash or commit, then ask me."}'
exit 0
fi
Der reason ist wichtig. Er wird dem Modell zurückgegeben, sodass der Agent statt zu taumeln "stashe oder committe, dann frag mich" liest und genau das tut. Eine stumme Blockierung lässt den Agenten Varianten ausprobieren, bis er eine Formulierung findet, die dein Regex übersehen hat. Erkläre ihm, warum.
PostToolUse — der Inspektor. Wird ausgelöst, nachdem ein Werkzeug ausgeführt wurde. Ich nutze es, um nach jeder Dateibearbeitung einen schnellen Lint oder Type-Check auszuführen und Fehler direkt zurückzuspeisen. Der Agent erhält den Typfehler zwei Sekunden nach dem Schreiben des Bugs, anstatt ihn drei Unteraufgaben später zu entdecken, wenn nichts mehr kompiliert. Diese Feedback-Schleife enger zu machen, hat die Codequalität meiner Agenten mehr verbessert als jede Prompt-Änderung.
Stop — der Ausschalter. Wird ausgelöst, wenn der Agent denkt, er ist fertig. Das ist deine letzte Chance zu sagen "nein, noch nicht." Ich überprüfe, ob Tests wirklich bestanden werden, bevor ich einen Lauf beenden lasse. Wenn nicht, blockiert der Stop-Hook das Beenden und teilt dem Agenten mit, sie zu reparieren. Hier lebt auch der Loop-Erkennungszähler von früher — zu viele Bearbeitungen, die ganze Sache anhalten.
Wo Hooks leben
Hooks werden in settings.json konfiguriert, nicht im Gedächtnis des Modells oder in deiner CLAUDE.md. Das verwirrt die Leute ständig. "Von nun an immer den Linter nach dem Bearbeiten ausführen" ist nicht etwas, das du dem Modell sagst — es wird es bis Runde 30 vergessen haben. Es ist ein PostToolUse-Hook, den das Harness jedes Mal ausführt. Automatisiertes, wiederholtes Verhalten gehört in settings.json. Präferenzen gehören ins Gedächtnis. Vermische sie nicht.
Schnelle Struktur, ohne Zeremonie:
{
"hooks": {
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{"type": "command", "command": "~/.claude/guards/git-guard.sh"}]
}]
}
}
Der matcher begrenzt den Hook auf ein Werkzeug. Begrenze ihn eng. Ein PreToolUse-Hook, der bei jedem Werkzeug ausgelöst wird, jede Eingabe parst und in die Shell wechselt, fügt dem gesamten Lauf Latenz hinzu. Das habe ich auf die harte Tour gelernt, nachdem ein Guard-Skript mit einem unnötigen Netzwerkaufruf jeder einzelnen Bash-Befehl in einer 300-Aufruf-Sitzung eine Sekunde hinzufügte.
Der Fehler, den alle machen
Sperrlisten. Dein erster Instinkt ist es, die schlechten Befehle zu sperren. rm -rf, git reset, DROP TABLE. Das Modell ist kreativer als deine Sperrliste. Es findet find . -delete, oder base64-kodiert den Befehl, oder nutzt ein Werkzeug, das du vergessen hast.
Erstelle stattdessen eine Erlaubnisliste für die sicheren Operationen, wenn möglich. Für einen Read-Only-Recherche-Agenten erlaube grep, glob, read, web_search und blockiere alles andere standardmäßig. Es ist mühsamer einzurichten. Es ist auch die einzige Version, die standhält, wenn das Modell kreativ wird. Eine Sperrliste schützt dich vor den Fehlern, die du dir vorgestellt hast. Eine Erlaubnisliste schützt dich vor denen, die du nicht vorgesehen hast.
Die Agenten, die in meiner Konfiguration unbeaufsichtigt laufen, teilen alle eine Eigenschaft: Ich vertraue den Hooks, nicht dem Modell. Das Modell ist klug und schnell und entscheidet gelegentlich, dass deine zwei Stunden Arbeit unordentlich aussehen. Der Hook ist dumm und ändert nie seine Meinung. Wenn etwas Unwiderrufliches auf dem Spiel steht, ist dumm und unveränderlich genau das, was du willst.
Schreibe den Türsteher, bevor du jemanden in den Club lässt.
