Max Nardit
Dev

Der unsichtbare Backslash: Wie ein Shell-Escaping-Bug 20 Releases lautlos kaputtmachte

Der Tauri-Action-Signing-Key wollte nach 20 funktionierenden Releases plötzlich nicht mehr. Der Fix: ein wörtlicher Backslash, vom Shell-Quote-Escaping ins Passwort geschmuggelt.

Es ist Freitagmorgen. Beetroot 1.6.5 ist fertig, großes Update, 554 Tests grün, Changelog geschrieben. Ich starte das Release-Skript. Build läuft in 4 Minuten durch. NSIS-Installer, MSI, alles gut. Letzter Schritt: die Artefakte für Auto-Update signieren.

incorrect updater private key password: Wrong password for that key

Okay. Tippfehler, vermutlich. Nochmal. Gleicher Fehler. Passwort aus der Secure Note kopiert. Gleicher Fehler.

Das Passwort hat sich nicht geändert. Die Key-Datei hat sich nicht geändert. Genau dieser Befehl hat in den letzten Monat 20 Releases signiert. Letzter erfolgreicher Build vor sechs Tagen.

Irgendetwas ist zwischen Samstag und heute kaputtgegangen, und ich habe keine Ahnung was.

Den Kaninchenbau runter

Die erste Stunde ist methodisch. Das Offensichtliche prüfen:

bash
# Key-Datei unverändert?
sha256sum beetroot-signing.key
# Stimmt mit dem Hash aus den Logs vom 24. März überein ✓
 
# Passwort byteweise korrekt?
echo -n 'the_password!' | xxd
# Sauberer Hex, keine versteckten Zeichen ✓
 
# NTFS-Timestamps auf der Key-Datei?
# Erstellt: 1. März. Modifiziert: 1. März. Nie angefasst. ✓

Frischen Test-Key mit demselben Passwort generiert: funktioniert tadellos. Die Krypto ist also nicht kaputt. Das Passwort stimmt. Die Key-Datei ist intakt. Sie passen einfach nicht mehr zusammen.

Ich probiere alles, was mir einfällt: Umgebungsvariablen, --password-Flag, TAURI_SIGNING_PRIVATE_KEY_PATH, PowerShell statt Bash, ein separates Git-Bash-Fenster, ein frisch kompiliertes cargo tauri signer, durch tr -d '\r\n' gepiped, um eventuell unsichtbare Zeilenenden zu entfernen.

Jeder Versuch liefert denselben Fehler.

Die Sackgassen

Das ist der Teil beim Debuggen, in dem du anfängst, brillant klingende Theorien zu bauen, die sich als komplett falsch herausstellen.

Erste Theorie: Windows Security Update KB5079473, installiert am 3. April. Genau zwischen dem letzten funktionierenden Build und heute. Mein KI-Assistent hat sogar eine überzeugende Argumentation aufgebaut: „Das Security Update hat das kryptographische Verhalten verändert." Klang etwa zehn Minuten lang plausibel. Aber Tauris Signer nutzt reines Rust (scrypt + Ed25519), nicht die Windows CryptoAPI. Und der Test-Key mit demselben Passwort hat einwandfrei funktioniert. Wenn Windows die Krypto kaputtgemacht hätte, würde nichts funktionieren.

Zweite Theorie: npm hat etwas aktualisiert. node_modules/.package-lock.json hatte einen frischen Timestamp vom 3. April. Verdächtig. Aber das eigentliche tauri-cli-Binary? Datei-Datum: 4. Februar. Version: 2.10.0. Unverändert.

Die dritte Theorie war die lustige. Ich habe in GitHub CI nachgeschaut: gleiches Passwort, gleicher Key, scheitert auch. Smoking Gun? Nein. Es war seit dem 3. März kaputt. Das GitHub Secret war von Anfang an falsch. Ich habe es nie gemerkt, weil alle 20 Releases lokale Builds waren. Ein komplett separater Bug, der sich in aller Offenheit versteckt hatte.

Zwei Stunden drin. Keine Spuren, nur eine wachsende Liste von Dingen, die nicht das Problem sind.

Der Durchbruch

Ich starre auf den Bildschirm, ohne Ideen. Dann fällt es mir ein: Der Quellcode von Claude Code liegt auf der Platte. Es ist JavaScript. Ich kann ihn lesen.

Ich verfolge den Code-Pfad, der ausgeführt wird, wenn Claude Code einen Bash-Befehl startet:

BashTool.tsx → Shell.ts → bashProvider.ts → shellQuoting.ts

Und in shellQuoting.ts finde ich diesen Kommentar:

typescript
// The shell-quote library incorrectly escapes ! to \! in these cases

Mein Puls geht hoch. Ich lese den Code drumherum. Die shell-quote-Library, die Claude Code zum Sanitizen jedes Befehls vor dem Wrappen in eval benutzt, ersetzt ! durch \!.

Schneller Sanity-Check:

javascript
const { quote } = require('shell-quote');
quote(["export PASSWORD='mypassword!' && echo test"]);
// → "export PASSWORD='mypassword\\!' && echo test"

Da haben wir's. Ein wörtlicher Backslash, lautlos vor das Ausrufezeichen geschmuggelt.

Die Entwickler wussten Bescheid. Für Heredocs und mehrzeilige Strings hatten sie einen Workaround gebaut, der shell-quote komplett umgeht. Einzeilige Befehle? Laufen weiter durch quote(). ! wird weiter zu \!.

Was tatsächlich passiert ist

Rekonstruiert aus den JSONL-Command-Logs von Claude Code.

  1. März. Ich generiere den Signing-Key über Claude Code. Der Befehl npx tauri signer generate fragt interaktiv nach einem Passwort. Ich tippe mypassword!. Aber Claude Code wickelt alles in eval, und davor verwandelt shell-quote ! in \!. Der Key wird mit dem Passwort mypassword\! verschlüsselt, ein wörtlicher Backslash, den ich nie getippt und nie gesehen habe.

  2. März bis 28. März. Zwanzig erfolgreiche Releases. Jedes Mal schickt Claude Code das Passwort durch dieselbe shell-quoteeval-Pipeline. Der Backslash wird jedes Mal hinzugefügt. Passwort stimmt jedes Mal überein. Alles läuft tadellos.

  3. April. Claude Code aktualisiert von 2.1.86 auf 2.1.92. In der neuen Version ist das !-Escaping gefixt. Das Passwort wird jetzt sauber durchgereicht, ohne Phantom-Backslash. Der Key erwartet aber weiterhin mypassword\!.

Das richtige Passwort passt nicht mehr.

Ich war nicht allein

Nachdem ich die Root Cause gefunden hatte, habe ich GitHub durchsucht. Tauri hat mindestens drei offene Issues mit demselben Symptom:

  • #13485, „Updater Signing with password from environment is broken." Ein User hat festgestellt, dass Einfügen vs. Tippen eines Passworts während der Key-Generierung unterschiedliche Ergebnisse liefert. Strg+V im VS-Code-Terminal hat die wörtlichen Zeichen ^V statt des Clipboard-Inhalts eingefügt.
  • #14829, „Can not use empty string as password." Selbst -p "" funktioniert nicht konsistent über Windows, macOS und Linux.
  • #10488, „Invalid padding", „Wrong password" in CI. Keine brauchbaren Fehler-Diagnosen.

Die Kette von der Tastatur bis zum Signing-Tool: Terminal → Shell → eval → Umgebungsvariable → Signer. Das Passwort kann an jedem Schritt lautlos transformiert werden, und die Fehlermeldung („Wrong password") sagt dir nichts darüber, wo es schiefging.

Der Fix

Eine Zeile:

bash
export TAURI_SIGNING_PRIVATE_KEY_PASSWORD='mypassword\!'

Der Backslash war immer Teil des Passworts. Zwanzig Releases haben es bewiesen. Ich wusste nur nicht, dass er da war.

Die Ironie

Das shell-quote-Verhalten war ein Bug. ! zu \! zu escapen war falsch; ! innerhalb einfacher Anführungszeichen hat in Bash keine besondere Bedeutung. Claude Code 2.1.92 hat es gefixt. Das Passwort wird jetzt korrekt durchgereicht.

Aber mein Key wurde mit dem Bug-Verhalten generiert. Und zwanzig Releases wurden mit dem Bug-Verhalten signiert. Der Bug war meine Normalität. Den Bug zu fixen hat meinen Workflow zerschossen.

Das ist Hyrums Gesetz in Aktion: bei einer ausreichenden Anzahl von API-Nutzern werden alle beobachtbaren Verhaltensweisen deines Systems von irgendjemandem verwendet werden. Auch die Bugs.


Wenn dir das passiert

Wenn dein Tauri-Signing nach einem Update von Claude Code, einem Terminal-Emulator oder irgendeinem CLI-Wrapper plötzlich mit „Wrong password" scheitert, prüfe, ob shell-quote dein Passwort verbiegt:

bash
npm install shell-quote
node -e "const {quote} = require('shell-quote'); \
  console.log(quote(['echo hello!']))"
# Wenn der Output \! enthält, bist du betroffen

Falls ja, setze \ vor jedes ! im Passwort:

bash
# War (funktioniert nicht mehr):
export TAURI_SIGNING_PRIVATE_KEY_PASSWORD='mypassword!'
 
# Probier (wenn der Key über Claude Code / shell-quote generiert wurde):
export TAURI_SIGNING_PRIVATE_KEY_PASSWORD='mypassword\!'

Für zukünftige Keys nutze -p und überspring die interaktive Eingabe:

bash
npx tauri signer generate -w my-key.key -p "password_without_special_chars"

Vermeide !, $, `, \ und andere Shell-Metazeichen in Passwörtern, die von CLI-Tools verwendet werden. Selbst innerhalb einfacher Anführungszeichen kann eine Zwischenschicht sie „hilfsbereit" escapen.


Was ich daraus mitgenommen habe

Sechs Schichten zwischen meinem Druck auf Enter und der Ausführung des Befehls: Claude Code → shell-quote → eval → Bash → npx → tauri-cli. Jede davon kann die Daten lautlos transformieren, und eine hat es. Die Kosten eines Abstraktions-Stacks sind nicht nur Laufzeit, sondern die Angriffsfläche für unsichtbare Mutation.

Logs haben mich gerettet. Claude Code speichert die komplette Befehlshistorie in JSONL-Dateien, also konnte ich exakt rekonstruieren, was am 1. März ausgeführt wurde, und es mit dem 4. April vergleichen. Ohne diese Logs hätte ich einen neuen Key generiert und damit Auto-Update für alle kaputtgemacht, die Beetroot bereits installiert haben.

Wenn ein Tool einen --password- oder -p-Flag hat, nutze ihn. Interaktive Prompts, die durch Terminal-Multiplexer, KI-Coding-Assistenten oder Shell-Wrapper geroutet werden, sind ein Korruptionsvektor. Zu viele „hilfsbereite" Schichten zwischen deinen Tastenanschlägen und dem Tool, das sie tatsächlich konsumiert.

Der CI-Workflow, den ich vor Monaten aufgesetzt hatte, war seit dem 3. März kaputt. Ich habe ihn nie gefixt, weil lokale Builds funktionierten. Wären die Releases durch CI gelaufen, wäre die Umgebung gepinnt gewesen und ein lokales Tool-Update hätte nichts kaputtmachen können. Das ist das Nächste, das ich fixe.

Die billigste Lektion ist die nützlichste: Wenn zwanzig Dinge funktioniert haben und das einundzwanzigste scheitert, liegt die Veränderung in der Umgebung, nicht in den Daten. Etwas zwischen Tastatur und Tool hat sich bewegt. Finde, was sich bewegt hat.

Beetroot 1.6.5 ist am gleichen Tag rausgegangen. Der Backslash ist jetzt im Release-Runbook dokumentiert.

Diskussion

Hier gibt es keine Kommentarspalte. Diskussionen laufen auf X.

Max Nardit

Max Nardit

@mnardit

Weitere Artikel

Tauri Signing Key durch Shell-Quote-Backslash kaputt