Zum Inhalt springen
blueforce.ch Weniger Agentur. Mehr Mensch.
,

Hooks für Webentwickler: warum deine KI eine Handbremse braucht

Claude Code kennt Hooks wie WordPress: kleine Skripte, die vor jedem Werkzeugaufruf entscheiden. Warum eine Bitte in der CLAUDE.md nicht reicht und was 79 Testfälle über echten Schutz verraten.

Blauer Teaser mit Schutzschild vor einem Code-Fenster, umgeben von den Schaltern ALLOW, ASK und DENY

Als WordPress-Entwickler weisst du längst, was ein Hook ist. add_action, add_filter, ein Ereignis, ein eigener Code, der sich dort einklinkt. Du greifst nicht in den Core ein, du hängst dich an eine definierte Stelle im Ablauf.

Genau dieses Prinzip gibt es seit einer Weile eine Ebene tiefer: nicht im CMS, sondern im Werkzeug, mit dem du das CMS baust. Claude Code kennt Hooks. Und wer mit einer KI im eigenen Projektordner arbeitet, sollte sie kennen, bevor die erste Datei auf dem Live-Server landet, die dort nicht hingehört.

Das Problem: eine Bitte ist keine Regel

Die meisten von uns haben ihrem KI-Assistenten schon einmal eine Datei mit Anweisungen hingelegt. Bei Claude Code heisst sie CLAUDE.md. Da steht dann so etwas drin: «Bearbeite niemals Dateien in wp-includes.» Oder: «Führe nie ein Deploy aus, ohne vorher zu fragen.»

Das funktioniert erstaunlich oft. Aber es ist und bleibt eine Bitte. Ein Sprachmodell liest sie, gewichtet sie gegen alles andere im Kontext und hält sich in aller Regel daran. In aller Regel. Nach zwei Stunden Arbeit, dreissig Dateien und einer langen Fehlersuche kann eine Anweisung vom Anfang der Sitzung leiser werden.

Ein Hook ist keine Bitte. Ein Hook ist ein Shell-Skript, das ausgeführt wird, bevor oder nachdem etwas passiert. Es entscheidet nicht nach Sprachverständnis, sondern nach if. Es vergisst nichts, es gewichtet nichts, es ermüdet nicht. Der Unterschied ist derselbe wie zwischen einem Vermerk im Pflichtenheft und einem Validierungsfehler im Formular.

Für uns Webentwickler ist dieser Unterschied nicht akademisch. Eine PHP-Datei mit einem fehlenden Semikolon macht aus einer Website einen HTTP 500. Eine kaputte theme.json legt den Block-Editor lahm, ohne dass PHP je meckert. Ein versehentlich gepatchter Core überlebt das nächste Update nicht. Und rm -rf mit dem falschen Pfad braucht keine weitere Erklärung.

Wie Hooks in Claude Code funktionieren

Das Modell ist überschaubar. Claude Code feuert bei bestimmten Punkten im Ablauf Ereignisse ab. Für den Alltag reichen vier davon:

PreToolUse feuert, bevor ein Werkzeug ausgeführt wird, also bevor eine Datei geschrieben oder ein Befehl abgesetzt wird. Hier kannst du eingreifen und ablehnen.

PostToolUse feuert danach. Zu spät zum Verhindern, aber genau richtig zum Prüfen: Formatierer, Linter, Tests.

Notification feuert, wenn Claude Code auf dich wartet.

SessionStart feuert beim Start einer Sitzung, etwa um Kontext einzuspeisen.

Dein Hook bekommt die Angaben zum Ereignis als JSON auf stdin: welches Werkzeug, welcher Befehl, welche Datei. Er antwortet auf zwei mögliche Arten. Entweder über den Rückgabewert: 0 heisst «kein Einwand», 2 heisst «blockieren», und was dabei auf stderr steht, bekommt das Modell als Fehlermeldung zu sehen. Oder über JSON auf stdout, was mehr Kontrolle gibt:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "ask",
    "permissionDecisionReason": "Live-Deploy: vorher Backup prüfen."
  }
}

deny lehnt ab, ask fragt dich, allow winkt durch. Die Konfiguration liegt in .claude/settings.json im Projekt oder in ~/.claude/settings.json für alle Projekte.

Das war die ganze Theorie. Jetzt zur Praxis.

Fünf Hooks, die sich für WordPress-Arbeit lohnen

1. Syntaxprüfung nach jedem Schreibvorgang

Der nützlichste Hook überhaupt und gleichzeitig der einfachste. Nach jedem Schreibvorgang läuft php -l über die Datei. Findet es einen Syntaxfehler, geht die Meldung mit Rückgabewert 2 zurück an das Modell, das den Fehler in derselben Runde korrigiert. Du merkst nichts davon, ausser dass die Klasse Fehler verschwindet, die sonst erst der Browser zeigt.

Dasselbe gilt für JSON. Eine ungültige theme.json bemerkt kein PHP-Linter, aber jq empty in einer Zeile:

case "$file" in
  *.php)
    out=$(php -l "$file" 2>&1) || fail "PHP-Syntaxfehler in $file" "$out" ;;
  *.json)
    out=$(jq empty "$file" 2>&1) || fail "Ungültiges JSON in $file" "$out" ;;
esac

2. Pfadschutz

Es gibt Verzeichnisse, in denen von Hand nichts geändert wird: vendor, node_modules, wp-admin, wp-includes, dist, build. Und es gibt Dateien, bei denen eine Rückfrage angebracht ist, auch beim Lesen: wp-config.php, .env, alles mit secrets im Namen, Schlüsseldateien.

case "$file" in
  */vendor/*|*/node_modules/*)
    decide deny "Abhängigkeiten werden nicht von Hand editiert: $file" ;;
  */wp-admin/*|*/wp-includes/*)
    decide deny "Core wird nicht gepatcht, nimm Hook, Filter oder Child-Theme: $file" ;;
esac

Beachte den Unterschied zwischen Lesen und Schreiben. vendor lesen muss erlaubt bleiben, sonst kann dir niemand mehr erklären, was eine Bibliothek tut. Nur das Schreiben wird abgelehnt.

3. Bremse für gefährliche Befehle

Löschen von / oder ~ wird hart blockiert. Rückfrage gibt es bei Live-Deploys, Datenbankbefehlen mit Datenverlust, git push --force, git reset --hard, rm -rf und chmod 777. Nichts davon ist verboten, du wirst nur gefragt.

case "$cmd" in
  *deploy*live*|*deploy*prod*)
    decide ask "Live-Deploy: vorher Backup prüfen und Staging bestätigen." ;;
  *"wp db reset"*|*"DROP DATABASE"*)
    decide ask "Datenbankbefehl mit Datenverlust-Risiko." ;;
esac

4. Kein Eingriff in eine entfernte Datenbank ohne Snapshot

Der Hook, der mir am meisten Ruhe gibt. WordPress-Inhalte leben in der Datenbank, pro Umgebung, ohne Merge. Ein wp post update über SSH auf der Live-Seite überschreibt einen Stand, den es sonst nirgends mehr gibt.

Der Hook prüft deshalb: Geht der Befehl auf ein entferntes System, und schreibt er in die Datenbank? Wenn ja, verlangt er einen Snapshot, der jünger ist als eine halbe Stunde. Fehlt der, wird abgelehnt, nicht nachgefragt.

Zwei Feinheiten, die den Unterschied machen zwischen «blockt alles» und «blockt das Richtige»: wp plugin update läuft durch, denn das ist Code, keine Inhalte. Und wp db query wird nur gebremst, wenn die Abfrage tatsächlich schreibt. Ein SELECT darf jederzeit laufen, sonst kann man nicht einmal mehr nachsehen, was man gerade anrichten würde.

5. Benachrichtigung

Kleinigkeit mit grosser Wirkung im Alltag: eine Systemmeldung, wenn Claude Code auf eine Freigabe wartet. Auf macOS über osascript, sonst über eine Terminalsequenz. Erspart das ständige Zurückschauen ins Terminal.

Sechs Dinge, die ich beim Testen gelernt habe

Und jetzt der Teil, der den Unterschied macht zwischen «Hooks eingerichtet» und «Hooks, die tatsächlich schützen». Ich habe das Set mit inzwischen 79 Testfällen durchgeprüft. Sechs Befunde, die man nicht sieht, wenn man nur die Konfiguration hinschreibt und zufrieden ist.

Ein fehlendes Werkzeug schaltet den Schutz still ab. Meine Skripte lesen den Eingabe-JSON mit jq. Ist jq nicht installiert, liefert der Aufruf eine leere Zeichenkette, kein Muster greift, das Skript endet mit 0. Aus Sicht von Claude Code: kein Einwand. Der gesamte Schutz war wirkungslos, ohne eine einzige Fehlermeldung. Ein Wächter, der bei Ausfall lautlos die Tür aufmacht, ist schlimmer als keiner, weil du dich auf ihn verlässt. Die Lösung sind drei Zeilen am Anfang jedes Skripts, die das Fehlen melden.

Der Pfadschutz war über die Shell umgehbar. Ein Hook auf die Datei-Werkzeuge greift bei Edit und Write. Er greift nicht bei cat > wp-includes/version.php, denn das ist ein Bash-Aufruf. Wer Pfade schützt, muss beide Wege abdecken.

Anführungszeichen hebeln Mustervergleiche aus. rm -rf / wurde blockiert, rm -rf "/" nicht, weil das Muster den Schrägstrich direkt nach dem Leerzeichen erwartete. Solche Lücken findet man nicht durch Nachdenken, sondern durch einen Testfall.

Zu enge Matcher sind eine unsichtbare Lücke. Ich hatte auf Edit|Write gefiltert. MultiEdit und NotebookEdit liefen damit ungeprüft durch. Der Hook war korrekt, er wurde nur nie aufgerufen.

Plattformannahmen rächen sich. Meine erste Benachrichtigung nutzte eine Terminalsequenz, die iTerm2 und Ghostty kennen, Terminal.app aber nicht. Auf dem Zielsystem passierte schlicht nichts.

Der gefährlichste Fall enthielt das Wort nicht, auf das ich filterte. Mein Datenbankschutz erkannte entfernte Eingriffe daran, dass ssh im Befehl steht. Das deckt wp post update über SSH ab und fühlte sich vollständig an. Nur kommt in

scp lokal.sql server:/pfad/live.sql

das Wort ssh überhaupt nicht vor. Ausgerechnet der destruktivste Fall, eine lokale Datenbankdatei über die produktive zu schieben, wäre ungebremst durchgelaufen. Nicht wegen eines Denkfehlers in der Logik, sondern weil ich vom Werkzeug auf die Gefahr geschlossen hatte statt umgekehrt. Der Hook prüft jetzt ssh, scp und rsync, und bei den beiden letzten zusätzlich, ob eine Datenbankdatei im Spiel ist.

Die Lehre aus allen sechs Punkten ist dieselbe: ein Hook, den du nicht getestet hast, ist eine Vermutung. Und weil ein Hook nur bei Gefahr sichtbar wird, merkst du im Alltag nie, dass er nicht funktioniert. Deshalb gehört zu jedem Hook-Set ein Prüfskript, das dem Hook realistische Eingaben vorlegt und die Entscheidung mit der erwarteten vergleicht. Bei mir hat genau dieses Prüfskript später einen zerschossenen regulären Ausdruck aufgedeckt, der sonst still alle Bash-Befehle blockiert hätte.

Wo Hooks nicht hingehören

Zwei ehrliche Einschränkungen, damit du dich nicht auf etwas Falsches verlässt.

Ein Hook ist kein Sicherheitsmechanismus. Er ist ein Stolperdraht. Wer einen Pfad über eine Variable zusammensetzt oder eine ungewöhnliche Schreibweise wählt, kommt daran vorbei. Für harte Sperren ist das Berechtigungssystem zuständig, also der permissions-Block in derselben Konfigurationsdatei. Hooks sind für die Fälle da, in denen du dich selbst vor Flüchtigkeitsfehlern schützt, nicht vor einem Angreifer.

Und Hooks laufen mit deinen Rechten. Was du dort hineinschreibst, ist ausführbarer Code, der bei jedem passenden Werkzeugaufruf startet. Halte sie kurz, halte sie schnell.

Wie schnell das kippt, habe ich selbst gemerkt. Beim Erweitern der Sperrliste startete mein Pfadschutz plötzlich pro geprüftem Muster einen eigenen Unterprozess. Aus gut zwanzig Millisekunden wurden fast vierzig, und zwar bei einem Hook, der bei jedem einzelnen Dateizugriff anspringt. Behoben mit reiner Textersetzung in der Shell statt eines externen Aufrufs. Miss deine Hooks, sonst zahlst du den Preis bei jedem Tastendruck.

Das Beispiel-Package

Das komplette Set besteht aus sechs Dateien im Hook-Ordner: einer gemeinsamen Bibliothek mit der Werkzeugprüfung, den vier Schutz-Hooks, dem Benachrichtiger, dazu die settings.json und das Prüfskript.

Es liegt offen auf GitHub: github.com/blueforce/claude-hooks-wordpress, aktuell Version 1.1.1, MIT-Lizenz. Du kannst also jede Zeile im Browser lesen, bevor du etwas herunterlädst. Bei Shell-Skripten, die sich in deine Werkzeugkette einklinken, halte ich das für das Mindeste.

Installation für alle Projekte:

brew install jq
mkdir -p ~/.claude/hooks
cp hooks/*.sh ~/.claude/hooks/
chmod +x ~/.claude/hooks/*.sh
xattr -d com.apple.quarantine ~/.claude/hooks/*.sh 2>/dev/null
sed 's|${CLAUDE_PROJECT_DIR}|'"$HOME"'|g' settings.json > ~/.claude/settings.json

Zwei Details, die beim ersten Versuch gerne Zeit kosten. Heruntergeladene Skripte verlieren das Ausführungsrecht und tragen unter macOS ein Quarantäne-Attribut, deshalb chmod und xattr. Und ${CLAUDE_PROJECT_DIR} zeigt immer auf das gerade offene Projekt. Liegen die Skripte im Home-Verzeichnis, muss der Platzhalter ersetzt werden, sonst sucht Claude Code sie am falschen Ort.

Danach Claude Code neu starten und /hooks eintippen. Dort stehen die konfigurierten Hooks samt Quelle und vollständigem Befehl.

Der Praxistest dauert eine Zeile:

echo '{"tool_name":"Bash","tool_input":{"command":"./deploy.sh live"}}' \
  | ~/.claude/hooks/guard-bash.sh

Kommt JSON mit permissionDecision zurück, greift die Regel. Kommt nichts, läuft der Befehl durch.

Fazit

Hooks in Claude Code sind dasselbe Versprechen wie Hooks in WordPress: du greifst nicht in fremden Code ein, du klinkst dich an einer definierten Stelle ein und behältst die Kontrolle.

Der Unterschied zur CLAUDE.md ist der zwischen Bitte und Regel. Und je schneller eine KI Code produziert, desto mehr lohnt sich eine Handbremse, die nicht diskutiert.

Fang mit zweien an: Syntaxprüfung nach dem Schreiben und Rückfrage vor dem Live-Deploy. Das sind zwanzig Zeilen und sie fangen die zwei Fehler ab, die im Webentwickleralltag am meisten kosten. Alles andere ergibt sich, wenn du merkst, was dich tatsächlich stört.

Und schreib den Testfall dazu. Ein ungetesteter Wächter ist keiner.