TIPS 05 · GUARDIA

Der Wächter

Zwei Hooks für Claude Code: Einer fragt dich, bevor ein Befehl Dateien löscht, und blockiert die unumkehrbaren, der andere lässt nach jeder Änderung die Tests laufen.

Was es ist und warum es zählt

Das sind die zwei Hooks, die ich in meinen Projekten nutze. Ein Hook ist ein Befehl, den Claude Code an einem genauen Punkt der Sitzung von selbst ausführt. Der Unterschied zu einer Anweisung im Chat oder in der CLAUDE.md liegt genau hier: Eine Anweisung kann Claude nach zwei Stunden Arbeit aus den Augen verlieren. Einen Hook nicht. Claude Code führt ihn jedes Mal aus, ob Claude daran denkt oder nicht.

Der Wächter ist da, weil sich manche Befehle nicht rückgängig machen lassen: Sie löschen Dateien, ohne über den Papierkorb zu gehen, schreiben die Git-Historie neu, leeren eine Datenbanktabelle. Ein falscher Pfad oder eine falsch verstandene Bitte reicht.

Mein Wächter macht zwei Dinge. Wenn ein Befehl Dateien löscht, fragt er dich nach einer Bestätigung, auch im auto-Modus, in dem Claude arbeitet, ohne dich etwas zu fragen. Wenn er unumkehrbar ist, etwa die ganze Festplatte löschen oder einen Push erzwingen, blockiert er ihn einfach.

Die Tests nach jeder Änderung sind für das andere klassische Problem: Claude ändert eine Datei, sagt dir „fertig“, und hat dabei woanders etwas kaputt gemacht. Mit dem Hook kommt das Testergebnis sofort bei ihm an, solange er noch weiß, was er angefasst hat.

1. Der Wächter: vor jedem Befehl

Der erste Hook nutzt das Ereignis PreToolUse, das auslöst, bevor Claude ein Werkzeug benutzt. Mit dem Filter „Bash“ schaut er nur auf Terminalbefehle. Leg im Projektordner die Datei .claude/hooks/waechter.sh an und füg Folgendes ein:

Die Datei
#!/bin/bash
# waechter.sh: fragt vor dem Löschen, blockiert das Unumkehrbare
command -v jq >/dev/null || { echo "Blockiert: jq fehlt (brew install jq)" >&2; exit 2; }
CMD=$(jq -er '.tool_input.command') || { echo "Blockiert: Eingabe nicht lesbar" >&2; exit 2; }
NO='rm[[:space:]]+-[a-z]*r[a-z]*[[:space:]]+(/|~/?|\$HOME/?)\*?([[:space:]]|$)|git[[:space:]]+push([[:space:]].*)?[[:space:]](-f|--force)|git[[:space:]]+reset[[:space:]]+--hard|drop[[:space:]]+(table|database)'
if grep -Eiq "$NO" <<<"$CMD"; then echo "Blockiert: nicht umkehrbarer Befehl ($CMD)" >&2; exit 2; fi
ASK='^[[:space:]]*((sudo|xargs)[[:space:]]+)*(rm|rmdir|unlink)([[:space:]]|$)|^[[:space:]]*find[[:space:]].*[[:space:]]-delete'
if tr ';&|()`' '\n\n\n\n\n\n' <<<"$CMD" | grep -Eq "$ASK"; then
  jq -n --arg r "Der Wächter: Dieser Befehl löscht Dateien. Bestätigen?" \
    '{hookSpecificOutput: {hookEventName: "PreToolUse", permissionDecision: "ask", permissionDecisionReason: $r}}'
fi
exit 0

So funktioniert es: Vor jedem Befehl übergibt Claude Code dem Skript den Befehl als JSON, im Feld tool_input.command. Das Skript kann auf drei Arten antworten:

  • Es endet mit Code 2: Der Befehl startet nicht, und Claude liest den Grund. Das passiert bei den Befehlen der Liste NO.
  • Es gibt ein kleines JSON mit permissionDecision „ask“ aus und endet mit 0: Claude Code zeigt dir die Berechtigungsabfrage mit dem Grund aus dem Skript, und der Befehl startet nur, wenn du Ja sagst. Das passiert bei den Befehlen der Liste ASK.
  • Es endet mit 0, ohne etwas auszugeben: Der Befehl läuft mit den normalen Berechtigungen weiter.

Die zwei Zeilen nach dem Kommentar blockieren alles, wenn jq fehlt, das Programm, das das JSON liest, oder wenn sich die Eingabe nicht lesen lässt. Auf dem Mac installierst du jq mit brew install jq.

Was er abfragt, was er blockiert, was er durchlässt

  • ASK, fragt nach: rm, rmdir, unlink und find … -delete, auch mit sudo oder xargs davor. Vor der Suche zerlegt das Skript den Befehl an ; & | ( ) und Backticks, so erwischt es auch das rm am Ende von cd ordner && rm -r build.
  • NO, blockiert einfach: rm -r auf /, ~ oder $HOME, also die ganze Festplatte oder den ganzen Benutzerordner; git push mit -f oder --force; git reset --hard; DROP TABLE und DROP DATABASE, groß oder klein geschrieben.
  • Lässt durch: git rm --cached, docker rm und alles andere, weil der Befehl dort git oder docker ist, nicht rm.

Die zwei Listen erweiterst du selbst. Zum Beispiel git clean, das die neuen Dateien löscht, die git noch nicht kennt: In meiner Version fehlt es. Wenn du es nutzt, setz es in ASK oder NO, zusammen mit den Befehlen, die in deinem Projekt Schaden anrichten.

Was er nicht sieht

Der Wächter schaut nur auf Bash-Befehle. Wenn Claude eine Datei leert, indem es sie mit dem Werkzeug Write neu schreibt, hält er das nicht auf. Für Dateien, die sich nie ändern dürfen, etwa .env, brauchst du eine deny-Regel in den Berechtigungen, zum Beispiel Edit(./.env), die für alle Werkzeuge gilt, die Dateien ändern. Oder einen zweiten Hook auf Write.

2. Die Tests: nach jeder Änderung

Der zweite Hook nutzt PostToolUse, das auslöst, nachdem ein Werkzeug seine Arbeit beendet hat. Mit dem Filter „Edit|Write“ startet er jedes Mal, wenn Claude eine Datei ändert oder anlegt. Leg .claude/hooks/test-nach-aenderung.sh an:

Die Datei
#!/bin/bash
cd "$CLAUDE_PROJECT_DIR" || exit 0
OUT=$(npm test 2>&1) && exit 0
echo "Tests nach der Änderung fehlgeschlagen:" >&2
echo "$OUT" | tail -n 30 >&2
exit 2

Setz deinen eigenen Testbefehl an die Stelle von npm test. Bestehen die Tests, siehst du nichts. Schlagen sie fehl, schickt das Skript Claude die letzten 30 Zeilen des Fehlers und endet mit Code 2.

Hier macht Code 2 die Änderung nicht rückgängig: Sie ist schon gemacht. Er sorgt dafür, dass Claude den Fehler liest und ihn sofort behebt. Das ist der Unterschied zwischen den beiden Hooks: Der Wächter kann vorher Nein sagen, dieser kann nur hinterher warnen. Und Claude wartet nach jeder Änderung, bis die Tests fertig sind: Wenn deine langsam sind, nimm ins Skript nur die schnellen.

3. Aktiviere sie

Mach beide Skripte ausführbar. Im Terminal, im Projektordner:

Die Zeile
chmod +x .claude/hooks/*.sh

Dann trägst du in .claude/settings.json, ebenfalls im Projektordner, die zwei Hooks ein:

Die Datei
{
  "hooks": {
    "PreToolUse": [
      { "matcher": "Bash", "hooks": [{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/waechter.sh" }] }
    ],
    "PostToolUse": [
      { "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/test-nach-aenderung.sh" }] }
    ]
  }
}

Wenn die Datei schon existiert, ersetz sie nicht: Füg den Teil „hooks“ neben dem ein, was schon drinsteht, und prüf die Kommas. $CLAUDE_PROJECT_DIR ist der Ordner, in dem du die Sitzung gestartet hast, so findet Claude Code die Skripte auch dann, wenn Claude in einem Unterordner arbeitet.

Wenn du nicht alles von Hand machen willst, bitte Claude darum:

Der Prompt
Leg in diesem Projekt die zwei Hooks aus dieser Anleitung an: einen PreToolUse-Hook auf Bash, der mich vor dem Löschen von Dateien um Bestätigung bittet und unumkehrbare Befehle mit Code 2 blockiert, und einen PostToolUse-Hook auf Edit und Write, der nach jeder Änderung [dein Testbefehl] ausführt. Trag sie in .claude/settings.json ein, ohne etwas zu entfernen, das schon drinsteht. Dann zeig mir die zwei Skripte und erklär mir, was jede Zeile macht.

4. Der Test

Zuerst teste ich den Wächter allein, ohne Claude. Im Terminal, im Projektordner, gebe ich ihm einen Testbefehl, so wie Claude Code es tun würde:

Die Zeile
echo '{"tool_input":{"command":"rm -rf testordner"}}' | .claude/hooks/waechter.sh; echo $?

Es sollte ein kleines JSON mit „ask“ erscheinen und dann 0. Mit git reset --hard statt rm -rf testordner sollte „Blockiert“ erscheinen und dann 2. Mit ls nur 0. Das Skript liest nur den Text: Hier wird nichts ausgeführt. Dieser Test ist wichtig, denn wenn der Pfad in settings.json falsch ist oder chmod +x fehlt, zeigt Claude Code nur einen Hinweis „hook error“, und der Befehl geht ohne Nachfrage durch.

Dann der echte Test. Öffne eine neue Sitzung: Meistens bemerkt Claude Code Änderungen an settings.json von selbst, aber so bist du sicher. Tipp /hooks ein und prüf, dass beide da sind, einer unter PreToolUse und einer unter PostToolUse. Dann bitte Claude:

Der Prompt
Leg einen leeren Ordner namens waechter-test an und lösch ihn dann.

Die Berechtigungsabfrage sollte erscheinen, mit dem Grund „Der Wächter: Dieser Befehl löscht Dateien“ und dem Etikett [settings], das dir sagt, woher die Frage kommt. Beim ersten Mal antwortest du Nein und prüfst, ob der Ordner noch da ist.

Für den Test-Hook:

Der Prompt
Ändere eine Zeile in einer Projektdatei so, dass ein Test fehlschlägt, und sag mir dann, was du nach der Änderung bekommen hast. Zum Schluss stell alles wieder her und prüf, dass die Tests bestehen.

Häufige Fehler

  • Aus Gewohnheit Ja sagen: Die Frage des Wächters ist genau dafür da. Lies den Befehl, bevor du antwortest.
  • Dem Wächter vertrauen, ohne ihn arbeiten gesehen zu haben: Bei einem falschen Pfad geht der Befehl mit einem bloßen Hinweis durch. Mach immer den Test von Hand mit echo.
  • Mit schon kaputten Tests starten: Jede Änderung bekommt einen Fehler, der nichts mit ihr zu tun hat. Bring die Tests zum Laufen, bevor du den Hook aktivierst, oder lass nur die laufen, die bestehen.

Checkliste

  • waechter.sh in .claude/hooks, mit deinen Listen ASK und NO
  • test-nach-aenderung.sh mit deinem Testbefehl
  • jq installiert
  • chmod +x auf beide Skripte
  • Hooks in .claude/settings.json eingetragen
  • Test von Hand mit echo: ask bei rm, 2 bei git reset --hard
  • Test in Claude Code: Berechtigungsabfrage für den Ordner, fehlgeschlagener Test gemeldet

Häufige Fragen

Und wenn nachts niemand antwortet?

Der Befehl bleibt bei der Abfrage stehen, bis du zurück bist, und nichts wird gelöscht. Das gilt auch im auto-Modus. Wenn du Claude Code dagegen ohne jemanden davor laufen lässt, zum Beispiel mit claude -p, kann die Abfrage nicht erscheinen: Claude Code lehnt den Befehl ab, und Claude liest den Grund.

Schützt mich der Wächter vor allem?

Nein. Er schaut auf den Text der Bash-Befehle, und derselbe Schaden lässt sich auch anders anrichten, zum Beispiel mit einem kleinen Python-Skript oder indem eine Datei mit Write neu geschrieben wird. Er senkt das Risiko. Das echte Sicherheitsnetz bleibt git: ein Commit vor langen Aufgaben und eine Kopie des Projekts online.

← Alle Anleitungen