Wird geladen
Wird geladen

Hooks

Hier ist das Problem, wenn du Claude in einer CLAUDE.md-Datei anweist, etwas zu tun: Es ist eine Bitte, keine Garantie. Du kannst schreiben „formatiere nach dem Bearbeiten immer" und Claude wird in der Regel darauf hören. In der Regel. Aber bei einem langen Lauf, den du nicht beobachtest, reicht „in der Regel" nicht aus. Ein Hook behebt das. Ein Hook ist deterministischer Code, der an einem festen Punkt in der Schleife ausgeführt wird, sodass er Verhalten garantieren kann, anstatt darauf zu hoffen. Er macht aus einer Regel, auf die „Claude in der Regel hört", eine, die „Claude nicht überspringen kann."

Das ist das ganze Versprechen. Schauen wir uns jetzt an, wie es tatsächlich funktioniert.

Die Hook-Events

Claude Code löst im Verlauf einer Sitzung rund 30 Hook-Events aus. Du musst nicht alle kennen. Es gibt eine kleine Handvoll, auf die du immer wieder zurückgreifen wirst, und sie entsprechen Punkten in der agentischen Schleife, an denen du eingreifen möchtest.

So fügen sie sich in die Schleife ein: Eine Sitzung beginnt, Prompts kommen herein, Tools werden aufgerufen, und der Turn endet schließlich. Jeder dieser Momente hat einen Hook, an den du Code hängen kannst.

Die, die es wert sind, gekannt zu werden:

  • PreToolUse wird vor einem Tool-Aufruf ausgelöst. Das ist dein Durchsetzungsprimitiv. Es ist dasjenige, das etwas stoppen kann, bevor es passiert.
  • PostToolUse wird nach einem erfolgreichen Tool-Aufruf ausgelöst. Hier landet normalerweise die automatische Formatierung oder ein automatisches Lint.
  • Stop wird ausgelöst, wenn Claude seinen Turn beenden möchte. Du kannst ablehnen und sagen „nein, du bist noch nicht fertig", wenn eine Bedingung nicht erfüllt ist. Es gibt ein entsprechendes SubagentStop für den Fall, dass ein Sub-Agent fertig ist.
  • PreCompact und PostCompact werden vor und nach der Komprimierung ausgelöst.
  • InstructionsLoaded wird ausgelöst, wenn eine CLAUDE.md- oder Regeldatei geladen wird. Praktisch, um zu überprüfen, was tatsächlich in den Kontext gelangt ist.
  • SessionStart wird zu Beginn ausgelöst und bereitet die Umgebung vor. Verwende die Quelle startup, wenn du es nur bei Neustarts haben möchtest.

Eine Sache, über die Leute stolpern: Um nach einer Komprimierung Kontext erneut einzuspeisen, verwende nicht PostCompact. Verwende SessionStart mit dem Matcher compact. Das ist dasjenige, dessen Ausgabe tatsächlich wieder in die Konversation gelangt.

PreToolUse: eine Entscheidung als JSON zurückgeben

PreToolUse ist der Ort, an dem die eigentliche Macht liegt, weil es einen Tool-Aufruf blockieren kann, bevor er ausgeführt wird. Die Art und Weise, wie du mit Claude kommunizierst, besteht darin, JSON auszugeben und mit Null zu beenden. Das Schlüsselfeld ist permissionDecision, und es nimmt einen von drei Werten an:

  • allow — den Aufruf durchlassen
  • deny — den Aufruf stoppen
  • ask — die Entscheidung an den Nutzer zurückgeben

Es gibt technisch gesehen einen vierten Wert, defer, aber er gilt nur für nicht-interaktive -p-Läufe, bei denen ein aufrufender Prozess das Tool pausiert und es später fortsetzt. Du wirst selten darauf zurückgreifen.

Die Form sieht so aus:

json
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "...",
    "updatedInput": {
      "command": "..."
    }
  }
}

Beachte updatedInput. Anstatt einen Aufruf zu blockieren, kannst du ihn umschreiben. So würdest du ein Secret aus einem Bash-Befehl entfernen und ihn trotzdem ausführen lassen. Ein Haken: updatedInput ersetzt das gesamte Input-Objekt, also musst du die Felder, die du nicht änderst, zurückgeben, sonst verlierst du sie.

Exit-Codes für Hooks, die kein JSON zurückgeben

Nicht jeder Hook muss JSON sprechen. Für einfachere Hooks erledigen Exit-Codes die Arbeit. Es gibt drei Zahlen, die wichtig sind.

  • 0 bedeutet Erfolg. Wenn die Standardausgabe JSON ist, parst Claude sie. Reiner Text wird bei den meisten Events ignoriert, aber bei SessionStart, UserPromptSubmit und UserPromptExpansion wird reiner Text dem Kontext hinzugefügt. Genau das macht einen Zustandserhaltungs-Hook funktionsfähig.
  • 2 ist ein blockierender Fehler. Die Standardfehlerausgabe wird als Kontext an Claude zurückgegeben. Das ist fast überall der blockierende Exit-Code.
  • Alles andere ist nicht blockierend. Die Standardfehlerausgabe wird protokolliert, und Claude macht weiter.

Der, der Leute überrascht, ist Exit-Code 1. Er fühlt sich wie ein Fehler an, blockiert aber nicht. Claude führt den Befehl trotzdem aus. Wenn du also etwas stoppen wolltest, verwende Exit 2, nicht 1.

Noch ein paar weitere Feinheiten. Exit 2 kann sogar Stop blockieren, womit du Claude mitteilst, dass es noch nicht fertig ist. Aber PostToolUse wird ausgelöst, nachdem das Tool bereits ausgeführt wurde, daher ist eine Blockierung dort zu spät, um den Aufruf zu stoppen, obwohl sie trotzdem Text an Claude zurückgeben kann. Und ein paar Events ignorieren Blockierung vollständig, wie Notification und SessionStart. SessionStart zeigt deine Standardfehlerausgabe nur dir an, Notification ignoriert sie, und beide machen trotzdem weiter.

Ein echter Schutzmechanismus: redigieren statt blockieren

Lass uns das mit etwas Praktischem verbinden. Angenommen, du möchtest einen PreToolUse-Schutzmechanismus für das Bash-Tool. Der Matcher wählt das zu beobachtende Tool aus, und eine optionale if-Klausel kann es auf einen bestimmten Befehl eingrenzen.

Der naheliegende Schritt ist, deny zurückzugeben und einen gefährlichen Aufruf zu stoppen. Das ist gut. Aber der weniger bekannte und interessantere Schritt ist, updatedInput zurückzugeben, um den Aufruf umzuschreiben. So entfernst du ein Secret aus einem Befehl und lässt ihn trotzdem ausführen, anstatt ihn einfach abzulehnen.

So sieht das in der Praxis aus. Claude wird gebeten, einen Befehl auszuführen, der ein echt aussehendes Secret enthält. Der Hook fängt ihn ab, erkennt das Muster sk_live_ und tauscht es gegen einen Platzhalter aus, bevor der Befehl jemals ausgeführt wird.

Der Befehl wurde trotzdem ausgeführt. Die Arbeit wurde trotzdem erledigt. Aber das Secret hat es nie durchgeschafft. Das ist der Unterschied zwischen Blockieren und Redigieren, und es ist die Art von Sache, die ein Hook jedes einzelne Mal durchsetzen kann.

Zustand über eine Komprimierung hinweg erhalten

Noch ein Muster, das es wert ist, eingerichtet zu werden. Wenn Claude eine lange Konversation komprimiert, verwirft es viele Details. Ein SessionStart-Hook mit dem Matcher compact wird direkt nach der Komprimierung ausgeführt. Lass ihn eine kurze Zusammenfassung der Dateien ausgeben, an denen du gearbeitet hast. Diese Zusammenfassung gelangt zurück in den Kontext, sodass Claude dort weitermacht, wo es aufgehört hat, anstatt kalt zu starten.

Zusammenfassung

Hooks machen aus einer Regel, der Claude in der Regel folgt, eine, der es immer folgt. Geh über die automatische Formatierung hinaus: Schütze Tools mit PreToolUse, kontrolliere den Turn mit Stop und erhalte den Zustand über eine Komprimierung hinweg. Die Einrichtung erfordert anfangs etwas Aufwand, zahlt sich aber beim ersten Mal aus, wenn sie etwas bei einem Lauf abfängt, den du nicht einmal beobachtet hast.