Schutzgeländer für KI-Befehle: PreToolUse-Hook gegen zerstörerische Shell-Aufrufe

Schema: Der PreToolUse-Hook schutz-hook.py prüft jeden Bash-Befehl des KI-Assistenten vor der Ausführung und blockt zerstörerische Aufrufe sowie Neustarts im Backup-Fenster

Verfasst von

in

In meinem Homelab läuft Claude Code in einem eigenen Admin-Container (LXC) und verwaltet von dort aus die Proxmox-Knoten, Container und Dienste per SSH. Damit ich nicht jeden einzelnen Lese- oder Wartungsbefehl abnicken muss, ist der SSH-Aufruf in der Konfiguration pauschal freigegeben. Ein vorgeschalteter Hook (~/scripts/schutz-hook.py) prüft deshalb jeden Shell-Befehl vor der Ausführung und blockiert zerstörerische Eingriffe sowie Neustarts während des nächtlichen Backup-Fensters.

Schema: Der PreToolUse-Hook schutz-hook.py prüft jeden Bash-Befehl des KI-Assistenten vor der Ausführung und blockt zerstörerische Aufrufe sowie Neustarts im Backup-Fenster

Ausgangslage: 8.398 Shell-Befehle in neun Tagen

Mitte Oktober habe ich die Sitzungsverläufe des Assistenten aus neun Betriebstagen (6. bis 14. Oktober) ausgewertet. In diesen 73 Sitzungen kamen auf 1.152 Nachrichten von mir insgesamt 21.180 Modellschritte, also rund 18 selbstständige Schritte je Auftrag. Das mit Abstand häufigste Werkzeug war die Shell mit 8.398 Bash-Aufrufen, weit vor lesenden Dateizugriffen (360) oder Bearbeitungen (368).

In der Datei ~/.claude/settings.json stand dafür die Freigabe Bash(ssh *). Wenn du dem Assistenten damit erlaubst, Kommandos per SSH ohne Rückfrage auf allen erreichbaren Systemen abzusetzen, läuft die Fehlersuche über mehrere Hosts und Container hinweg ohne ständige Unterbrechungen. Gleichzeitig gab es auf dieser Ebene aber keinerlei Sperre gegen Befehle wie qm destroy, zpool destroy oder ein versehentliches rm -rf auf einem Proxmox-Knoten.

Umgekehrt hat der eingebaute Auto-Modus des Assistenten im selben Zeitraum 170-mal gewollte Schritte abgelehnt: je 15-mal das Ändern geteilter Ressourcen und dauerhafte Einrichtungen wie Cron-Einträge, 11-mal Schreibzugriffe auf entfernten Systemen sowie mehrfach die Rechtevergabe und das Schreiben in den Kennwort-Tresor. Aus diesen Ablehnungen hatten sich bereits 131 manuelle Hilfsskripte angesammelt.

Am 14. Oktober habe ich die Berechtigungen deshalb neu geordnet. Bash(ssh *) bleibt offen, und wiederkehrende Standardänderungen — etwa das Pflegen von Checkmk-Regeln, das Pushen der Dokumentation, neue Server im Netz 192.0.2.0/24 oder DHCP-Einträge auf beiden Domain Controllern — stehen als dauerhafte Freigaben in ~/CLAUDE.md. Im Gegenzug prüft ein eigener PreToolUse-Hook jeden Shell-Aufruf vorab gegen eine feste Sperrliste und gegen das nächtliche Backup-Fenster.

Zwei Regeln im PreToolUse-Hook

Eingebunden ist ~/scripts/schutz-hook.py als PreToolUse-Hook in ~/.claude/settings.json. Bevor das Bash-Werkzeug einen Befehl ausführt, übergibt Claude Code den Aufruf an das Python-Skript. Schlägt eine Regel an, lehnt der Hook die Ausführung ab, gibt der laufenden Sitzung den Ablehnungsgrund zurück und schreibt einen Eintrag ins Log.

Falls im Hook selbst ein unbehandelter Fehler auftritt, beendet sich das Skript mit Exit 0. Ein Defekt am Prüfskript legt dadurch nicht die gesamte Arbeit im Admin-Container lahm.

RegelGesperrte BefehleZeitraum
Sperrlisteqm destroy, pct destroy, pvesh delete auf einen Gast, zpool destroy, zpool labelclear, zfs destroy, mkfs*, mke2fs, blkdiscard, wipefs -a, sgdisk -Z / -o, dd of=/dev/…, rm -r auf /, oberste Systemordner, ~ oder das eigene Home-Verzeichnis, Umleitungen (>) auf /dev/sd*, /dev/nvme*, /dev/zd*immer
Backup-Fensterreboot, shutdown, poweroff, halt, systemctl reboot / poweroff, init 0 / 6, qm und pct mit reboot, shutdown, stop, reset, suspend oder migrate, apt / apt-get mit upgrade, dist-upgrade, full-upgrade, pveupgrade, Restart-Computer, Stop-Computer02:00–04:00 Uhr (Europe/Berlin)

Die Zeitsperre zwischen 02:00 und 04:00 Uhr schützt die täglichen Sicherungsläufe zum Proxmox Backup Server. Zwar pausiert auch der geplante Nachtlauf für unbeaufsichtigte Hintergrundaufgaben in dieser Zeit (seine Zeitfenster liegen bei 22:00 bis 02:00 Uhr und 04:30 bis 07:30 Uhr), doch eine spät gestartete oder überziehende Sitzung könnte sonst kurz nach zwei Uhr noch einen Container stoppen oder ein Kernel-Upgrade samt Neustart anstoßen. Der Hook fängt das direkt vor dem Shell-Aufruf ab, auch für Windows-Systeme über Restart-Computer und Stop-Computer.

Befehlsposition prüfen statt bloßer Textsuche

Eine reine Suche nach Teilwörtern in der Befehlszeile führt in der Praxis schnell zu Fehlalarmen. Das habe ich an einem anderen Prüfskript im selben Container erlebt, das vor dem nächtlichen Git-Abgleich nach Geheimnissen sucht: Eine harmlose Zeichenkette in einer abgelegten API-Antwort reichte, und der Abgleich brach ab, obwohl kein Geheimnis im Commit stand.

Würde schutz-hook.py nach demselben Prinzip arbeiten, könnte der Assistent weder mit grep in der Dokumentation nach zfs destroy suchen noch einen Logeintrag über ein apt upgrade schreiben. Das Skript wertet deshalb nur aus, was an einer Befehlsposition der Shell steht.

Vergleich von vier Shell-Aufrufen: schutz-hook.py erkennt gesperrte Befehle in SSH und Containern, lässt aber Simulationen und Suchbefehle durch

Steht an der ersten Position ein Fernzugriff oder ein Wrapper, schält der Parser den darin übergebenen Befehl Schicht für Schicht heraus und prüft ihn erneut:

  • Fernzugriffe über ssh host '…'
  • Befehlsausführungen in Gästen per pct exec … -- und qm guest exec … --
  • Verschachtelte Shell-Aufrufe wie bash -c, sh -c und Kommandosubstitutionen $(…)
  • Vorangestellte Hilfsprogramme wie sudo, timeout oder env

Reiner Text innerhalb von echo, Suchmuster in grep, Commit-Meldungen und Heredocs stehen nicht an einer Befehlsposition und gehen frei durch. Außerdem erkennt der Hook Simulationsschalter: Ein Trockenlauf mit apt-get -s oder eine reine Prüfung mit wipefs -n bleibt erlaubt.

# Wird vom Hook abgefangen (Befehlsposition innerhalb von ssh + sudo):
ssh root@192.0.2.10 'sudo qm destroy 105 --purge'

# Wird zwischen 02:00 und 04:00 Uhr abgefangen (Container-Exec + Subshell):
ssh root@192.0.2.10 'pct exec 104 -- bash -c "apt-get dist-upgrade -y"'

# Bleibt frei (Simulation bzw. reines Textargument):
ssh root@192.0.2.10 'apt-get -s dist-upgrade'
grep -rn "qm destroy" ~/doku/

Eine bewusste Grenze hat dieses Verfahren: Der Hook sieht nur die Befehlszeile, die das Bash-Werkzeug startet. Ruft der Assistent ein Python-Programm oder ein bestehendes Skript auf, das intern über eine Bibliothek — zum Beispiel per WinRM aus Python heraus — eigene Befehle an ein Zielsystem schickt, ist deren Inhalt für den Hook nicht sichtbar. Auch reguläre Cron-Jobs laufen außerhalb einer Claude-Sitzung und durchlaufen den Hook nicht.

Gegenprobe mit 8.485 Befehlen und 290 Selbsttests

Vor der Aktivierung am 14. Oktober habe ich schutz-hook.py gegen alle 8.485 früheren Bash-Aufrufe aus den vorhandenen Sitzungsverläufen getestet. Außerhalb der definierten Regeln trat kein einziger Fehlalarm auf. Geblockt worden wären in den neun Tagen unter anderem vier Aufrufe von qm destroy bzw. pct destroy beim Aufräumen von Test-VMs sowie mkfs und sgdisk während eines Plattenwechsels — genau die Eingriffe, die nicht unbemerkt nebenbei laufen sollen.

Für laufende Änderungen am Parser bringt das Skript einen eigenen Selbsttest mit:

python3 ~/scripts/schutz-hook.py --test

Am 18. Oktober habe ich den Hook einer Zweitprüfung unterzogen, um verbleibende Randfälle beim Parsen von Quoting und Verschachtelungen zu finden. Ursprünglich sollte ein zweites Sprachmodell diese Prüfung übernehmen; nachdem dessen Lauf jedoch zweimal am Zeitlimit (Exit 10) gescheitert war, hat Opus den Code überprüft. Dabei fanden sich drei Fehlalarme und drei Möglichkeiten, die Prüfung durch spezielle Schreibweisen zu umgehen. Beides ist seitdem behoben, und der integrierte Selbsttest deckt inzwischen 290 Testfälle ab.

Warum der Assistent die Sperre nicht umgeht

Ein naheliegender Einwand gegen einen lokalen Hook lautet: Wenn der Assistent im selben Container Schreibrechte hat, könnte er bei einer Ablehnung einfach ~/.claude/settings.json anpassen, den Hook abschalten oder den Befehl in ein Hilfsskript verpacken. In der Praxis verhindern das drei Schichten.

Auf der technischen Ebene schützt der eingebaute Auto-Modus von Claude Code die Steuerdateien vor dem Assistenten selbst. Versucht eine Sitzung, ~/CLAUDE.md, ~/.claude/settings.json, Hooks oder eigene Berechtigungen so zu ändern, dass sich ihre Befugnisse erweitern, lehnt der Auto-Modus den Schreibzugriff als Self-Modification ab (erstmals aufgetreten und dokumentiert am 15. Oktober). Auch der Versuch, einen Subagenten mit der Anweisung zu starten, Rückfragen zu vermeiden, wird dort als Bypass erkannt und gestoppt. Da Änderungen an settings.json und an Hooks sofort auf alle parallel laufenden Sitzungen wirken, spiele ich Anpassungen daran grundsätzlich selbst ein.

Auf der organisatorischen Ebene regeln ~/CLAUDE.md und die Vorgaben für den Leitstand (über den bis zu drei Hintergrund-Agenten gleichzeitig verschiedene Aufgabenblöcke abarbeiten), wie mit einer Sperre umzugehen ist: Bei einer Ablehnung durch den Schutz-Hook oder den Auto-Modus halten die Agenten sofort an und melden den Status WARTET AUF ADMIN oder BLOCKIERT. Selbst im Fall einer Notfalländerung — wenn ein Dienst ausgefallen ist und ich nicht erreichbar bin — darf der Assistent nur die kleinste schadensbegrenzende Maßnahme ergreifen (etwa die eigene letzte Änderung zurücknehmen oder einen hängenden Dienst neu starten); Schutz-Hook, Backup-Fenster und Auto-Modus gelten unverändert weiter.

Vor allem aber gibt es für jede gesperrte Aktion einen vorgesehenen Übergabeweg, sodass gar kein Anreiz zum Basteln entsteht: Der Assistent legt ein fertiges Skript in ~/scripts/manual/ ab, das ich selbst ausführe.

Übergabe per Admin-Skript in ~/scripts/manual/

Stößt eine Sitzung auf einen gesperrten Befehl oder auf eine Schranke des Auto-Modus, bereitet sie (meist über den eigenen Skill /vorbereiten) ein ausführbares Skript unter ~/scripts/manual/ vor. Jedes dieser Skripte enthält im Kopf einen Block WAS AUSGEFÜHRT WIRD, unterstützt den Schalter --zeigen für einen reinen Vorschaulauf und dokumentiert den Rückfallpfad. Ich lese das Skript gegen und starte es mit einem Einzeiler.

Beim ersten echten Lauf des Leitstands am 15. Oktober mit drei parallelen Opus-Agenten hat genau diese Grenze gegriffen: Als ein Agent die Zeitzone auf einem Domain Controller anpassen wollte und vom Auto-Modus gestoppt wurde, legte er das Skript in ~/scripts/manual/ ab und ging zum nächsten Punkt über. Ein zweiter Agent hielt an, weil eine Checkmk-Aktivierung sonst offene Änderungen eines anderen Administratorkontos mitveröffentlicht hätte, und hinterlegte ebenfalls ein manuelles Skript. Auch das scharfe Schalten der Cron-Einträge für den Nachtlauf lief über diesen Ordner, weil der Auto-Modus das direkte Bearbeiten der Crontab als Selbsterweiterung ablehnt.

Eine Schwäche dieses Übergabewegs habe ich erst am 20. Oktober bemerkt.

Am selben Tag fiel mir bei einem Skript zur VSS-Reparatur auf einem Domain Controller eine zweite Falle auf. Ich hatte das Skript mit --zeigen gestartet. Weil es auch im Vorschaumodus den vollständigen Schrittplan Zeile für Zeile ausgab und sich am Ende still mit Exit 0 beendete, sah die Konsolenausgabe wie ein erfolgreich durchgelaufener Eingriff aus. Die eigentliche Reparatur unterblieb dadurch zunächst unbemerkt. Als offener Punkt auf meiner Liste steht deshalb jetzt an, die Ausgabe von --zeigen und die echte Ausführung in allen Vorlagen für ~/scripts/manual/ eindeutig voneinander zu trennen.

Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht, deine IP-Adresse wird nicht gespeichert. Kommentare erscheinen erst nach Freigabe. Was mit deinen Angaben passiert, steht in der Datenschutzerklärung. Pflichtfelder sind mit * markiert.