Dev

Claude-Code-Permission-Regeln: zehn Umgehungen, geschlossen in drei Releases

Die Hälfte der zehn Fixes betrifft Garantien, die die Dokumentation gibt, nicht die Grenzen, die sie ohnehin einräumt. Zwei weitere sind Komponenten, die offen versagt haben, und für ein PreToolUse-Hook-Skript, das abstürzt oder hängt, ist offenes Versagen weiterhin das dokumentierte Verhalten.

Drei Claude-Code-Releases in drei Tagen, v2.1.287 bis v2.1.289, enthalten zehn Changelog-Zeilen mit derselben Form: Eine Deny-Regel, eine Ask-Regel, ein Hook oder die Permission-Obergrenze einer Organisation hätte etwas stoppen sollen und hat es nicht getan. Einzeln liest sich jede Zeile wie Routinewartung. Als Set gelesen zeigen sie, wo diese Schicht bricht, und das ist nicht dort, wo die Dokumentation davor warnt.

Die Doku ist offen darüber, was eine Bash-Regel ist. Sie matcht den Befehlstext, den Claude schreibt, also:

text
a deny or ask rule covers the invocation Claude usually produces and
isn't a security boundary around the program.

Dieselbe Seite nennt /bin/rm -rf build/ und bash -c 'rm -rf build/' als Formen, die eine Deny-Regel Bash(rm *) nicht stoppt. Keiner der zehn Fixes betrifft diesen Disclaimer. Fünf davon haben einen Satz gebrochen, den die Doku als Garantie formuliert, und zwei sind Komponenten, die offen versagt haben. Die übrigen drei liegen an den Rändern: eine Oberfläche, die die Doku selbst als Best Effort bezeichnet, ein Nutzer-Plugin, das Text überschreibt, den eine Organisation verwaltet, und eine Prüfung, die Shell-Arithmetik übersehen hat. Alles hier stammt aus Anthropics Changelog. Ich habe die Bugs nicht reproduziert; alle zehn sind ab 2.1.289 behoben.

Haben Claude-Code-Deny-Regeln unter Sandbox-Auto-Allow gehalten?

Nicht, wenn vorher eine Variablenzuweisung kam. Zwei Fixes in v2.1.289: Bash-Deny- und Ask-Regeln haben einen Befehl hinter einem Env-Var-Präfix mit expandiertem Wert übersehen und wurden komplett übersprungen, wenn eine nackte Variablenzuweisung vor dem Befehl stand. Das Beispiel aus dem Changelog selbst:

bash
TZ="$HOME" rm -rf build

Beides widerspricht der Doku direkt. Die Permissions-Seite sagt, eine Deny-Regel „matches past any leading assignment, so Bash(rm *) in deny still matches FOO=bar rm -rf tmp/“, und die Sandboxing-Seite sagt, dass auch im Auto-Allow-Modus gilt:

text
Explicit deny rules are always respected

Das dokumentierte Beispiel verwendet einen literalen Wert, der erste Bug brauchte einen, den die Shell expandiert. Der zweite ist schwerer einzuordnen: Der Changelog sagt „bare variable assignment“ und sonst nichts. Wenn damit die dokumentierte Form FOO=bar gemeint ist, dann war das eigene Beispiel der Doku genau der Fall, der unter Auto-Allow versagt hat.

Es gibt hier einen verlockenden Trost: Auto-Allow gilt nur für Befehle, die in der Sandbox laufen, also hat die Sandbox doch gehalten. Unter der Standard-Policy hilft das wenig. Die Sandbox begrenzt, wohin ein Befehl schreiben darf, und standardmäßig ist das Arbeitsverzeichnis beschreibbar, ein gewöhnlicher build-Ordner ist also Freiwild. In diesem Setup war die Deny-Regel das Einzige, was den Befehl beim Namen nannte.

Ein dritter Fix in v2.1.288 gehört direkt daneben. Die Permission-Prüfung fragt jetzt nach, bevor eine BASHPID-Zuweisung läuft, deren Wert die Shell als Arithmetik auswerten würde; früher ging das stillschweigend durch. Diese Zeile ist der gesamte öffentliche Stand, also rate ich nicht am Mechanismus herum. Meine Lesart: Er gehört zu einem Fall, den die Doku schon behandelt. Im Manual-Modus fragt ein Schreibzugriff auf spezielle Shell-Variablen wie PATH oder IFS nach, selbst in einem sonst rein lesenden Befehl. Das sind Werte, mit denen die Shell Dinge tut, die der Befehlstext nicht zeigt, und BASHPID-Arithmetik sieht nach einem weiteren davon aus.

Warum lief in Claude Codes bypassPermissions-Modus ein gefährliches rm ohne Nachfrage?

Weil eine der wenigen Schutzmaßnahmen, die den Bypass-Modus überleben, zwei Löcher hatte. Die Seite zu den Permission-Modi listet auf, was kein Modus automatisch genehmigt, darunter:

text
rm and rmdir removals targeting a critical path, which no allow rule
or PreToolUse hook "allow" approves

v2.1.288 hat ein gefährliches rm behoben, eines auf / oder das Home-Verzeichnis, das ohne Nachfrage lief, wenn es in einem bash -c- oder sh -c-Skript stand, im bypassPermissions-Modus oder unter einer Shell-Allow-Regel (#96300). v2.1.287 hat behoben, dass dasselbe rm seine Always-Ask-Absicherung verlor, wenn der Befehl zusätzlich Ausgabe auf einen ~- oder Wildcard-Pfad umleitete.

Dieser Boden zählt mehr als die meisten Regeln, die ein Nutzer schreibt, weil er eine der wenigen Prüfungen ist, die denen bleiben, die die Nachfragen abgeschaltet haben. Ich habe argumentiert, dass Nachfragen pro Befehl Operatoren zum Bypass-Flag treiben, und wer diesen Weg ohne eigene Deny-Regeln gegangen ist, hat sich auf genau diese Prüfung verlassen. Den Befehl in eine zweite Shell zu packen oder eine Umleitung anzuhängen, hat gereicht, um darüber hinwegzusteigen.

Haben Claude Codes Read-Deny-Regeln Dateien aus der IDE abgedeckt?

Nicht, wenn die Datei über einen Symlink kam. v2.1.289 hat behoben, dass Read-Deny-Regeln nicht für Dateien galten, die in der IDE über einen Symlink per @ erwähnt, geändert oder ausgewählt wurden. Das ist kein gebrochenes Versprechen. Die Doku sagt, eine Deny-Regel greift „when either the requested path or the file it resolves to matches“, sagt aber auch, Claude unternehme „a best-effort attempt“, Read-Regeln auf @-Erwähnungen und auf den Kontext anzuwenden, den eine verbundene IDE teilt. Die IDE ist ein zweiter Weg, auf dem Dateiinhalte das Modell erreichen, und die Doku war ehrlich darin, dass die Regel ihm nur lose folgt.

Das ist wieder das Abdeckungsproblem: Eine Prüfung bindet nur über die Pfade, die sie abdeckt, und jede neue Art, dem Modell Kontext zuzuführen, ist ein neuer Pfad.

Haben verwaltete Deny-Regeln in Claude Code bei zusammengesetzten Befehlen gegen einen Mod gehalten?

Nicht für einen verschachtelten Teil. Auf einer Maschine mit Managed Settings stehen Deny-Regeln standardmäßig über einem vom Nutzer installierten Mod; das war die eine klare Antwort darauf, was Mods an der Hook-Rangfolge ändern. Unabhängig davon sagt die Doku, Deny- und Ask-Regeln greifen „when any subcommand matches them, including a command nested inside a subshell, a command substitution, or a control-flow body“. v2.1.289 hat behoben, dass eine Deny- oder Ask-Regel auf einem verschachtelten Teil eines zusammengesetzten Befehls auf verwalteten Maschinen nicht gegen die Genehmigung eines Mods hielt. Legt man die beiden Sätze nebeneinander, ist die Deny-Hälfte dieses Bugs eine gebrochene Garantie. Die Ask-Hälfte ist weniger eindeutig, weil die Doku sagt, ein Mod könne an einer Ask-Regel vorbei genehmigen; der Changelog behandelt es auf verwalteten Maschinen trotzdem als Bug.

Wo hat Claude Codes Permission-System offen versagt?

An zwei Stellen, die nichts mit Shell-Parsing zu tun haben, plus ein dritter Eintrag, der kein Versagen beim Blockieren ist, aber auf dieselbe Liste gehört:

text
v2.1.288  PreToolUse and PermissionRequest hooks were skipped when matching
          them failed or the tool input could not be serialized to JSON.
          The call is now blocked.
v2.1.287  Organization per-tool permission ceilings were silently dropped
          for an MCP tool named __proto__.
v2.1.289  A user-installed plugin could rewrite the descriptions of an
          organization-managed MCP server's sign-in tools.

Mehr sagt der Changelog zu den letzten beiden nicht. __proto__ hat auf gewöhnlichen JavaScript-Objekten ein spezielles Legacy-Verhalten; das reicht, um die Klasse des Bugs zu erraten, aber nicht, um sie zu behaupten. Der Plugin-Eintrag ist keine Prüfung, die einen Aufruf durchgelassen hat. Es ist eine Komponente auf Nutzerebene, die Text überschreibt, den eine Organisation kontrolliert. Das zählt, weil die Beschreibung eines Tools das ist, was das Modell liest, wenn es entscheidet, ob und wie es das Tool aufruft.

Der Hook-Fix ist der, den man genau lesen sollte, denn er deckt weniger ab, als sein Wortlaut nahelegt. Er schließt Fehler beim Matching und bei der Serialisierung. Was passiert, wenn dein Hook-Skript selbst fehlschlägt, bleibt unverändert, und die Hooks-Referenz ist da eindeutig. Exit-Code 1 ohne gültiges JSON ist ein nicht blockierender Fehler, und die Aktion läuft weiter. Ein Skript, das gar nicht startet, landet am selben Ort:

text
For most hook events, the action proceeds. When you set up a policy
hook, watch for this notice on its first run: a mistyped path in
settings.json leaves the gate silently disabled.

Und bei PreToolUse gilt: Ein Command-Hook, der in ein Timeout läuft, „doesn't block the tool call“. Nur Exit 2 blockiert von sich aus. Wenn also ein Hook dein Gate ist, sorg dafür, dass jeder Fehler darin mit Exit 2 endet. Das naheliegende Bash-Idiom trap 'exit 2' ERR reicht nicht: Ohne set -E feuert der Trap nicht innerhalb einer Funktion, und eine ungebundene Variable unter set -u beendet das Skript mit 1, ohne ihn überhaupt auszulösen. Ein EXIT-Trap fängt jeden Exit ungleich null:

bash
#!/bin/bash
set -euo pipefail
trap 'rc=$?; [ "$rc" -eq 0 ] || exit 2' EXIT

Unter Bash 5.2 habe ich einen fehlenden Befehl auf oberster Ebene, einen innerhalb einer Funktion und eine ungebundene Variable geprüft: Alle drei enden mit diesem Header in Exit 2, und ein sauberer Lauf endet weiterhin mit 0. Der Trap fängt nur Fehler, die das Skript nicht selbst behandelt. Einen Fehler in einem if, nach || oder tief in einer Command Substitution musst du weiterhin selbst prüfen und in Exit 2 übersetzen, also teste den Hook mit einer fehlenden Abhängigkeit und kaputter Eingabe, bevor du ihm vertraust. Nichts im Skript deckt einen Hook ab, der nie startet oder der hängt: Achte beim ersten Lauf auf den Hinweis hook error und halte den Hook schnell.

Was sagen zehn Claude-Code-Fixes über Deny-Regeln als Sicherheitsschicht?

Nach Mechanismus statt nach Doku-Versprechen geschnitten, fallen die zehn in zwei Gruppen. Die eine liegt dort, wo die Prüfung auf Shell-Semantik trifft: Expansion, Zuweisung, Arithmetik, eine verschachtelte Shell, eine Umleitung, ein Unterbefehl in einem zusammengesetzten Befehl. Die andere liegt dort, wo eine Komponente auf einen Pfad trifft, den niemand geprüft hat, oder offen versagt: die IDE, ein Hook, der nicht gematcht werden konnte, ein Objektschlüssel, ein Plugin, das verwalteten Text überschreibt. Meine Wette ist, dass die erste Gruppe weiter Fixes produziert. Ihr Raum ist die Grammatik der Shell, und eine Prüfung über Befehlstext muss jedes Konstrukt vorwegnehmen, das die Shell akzeptiert.

Also würde ich Deny- und Ask-Regeln so lesen, wie die Doku es verlangt: als Steuerung für die Befehle, die Claude normalerweise schreibt, und zwar als nützliche Steuerung. Eine Regel, die halten muss, egal wie der Befehl geschrieben ist, gehört an einen Ort, an dem Text keine Rolle spielt: die eigenen Dateisystem- und Netzwerkgrenzen der Sandbox oder ein Credential, das die Session nie bekommen hat. Diese Grenzen lecken auch, und in den Fällen, über die ich geschrieben habe, leckten sie über Dienste, die dem Agenten für eine andere Aufgabe gegeben wurden, nicht darüber, wie er einen Befehl geschrieben hat. Manche Regeln kann aber nur Text benennen, und rm -rf build ist eine davon. Die bleiben ihrer Natur nach Steuerung, und der eigentliche Schutz für build ist das, was nötig wäre, um es zurückzuholen.

Diskussion

Hier gibt es keine Kommentarspalte. Diskussionen laufen auf X.

Max Nardit

Max Nardit

@mnardit

Weitere Artikel