Dein Agent kann eine laufende SQLite-Datenbank byteweise zerstören — jetzt verweigert Hermes den Schreibvorgang


Es ist Mittwochnachmittag, und du bittest Hermes, ein kleines Konfigurationsskript in einem deiner Projekte anzupassen. Im selben Verzeichnis liegt zufällig eine kanban.db — oder die app.db deiner Anwendung. Er bearbeitet das Skript in wenigen Sekunden, du machst weiter. Am nächsten Morgen startet die Anwendung nicht mehr: database disk image is malformed. Du hast die Datenbank nie gelöscht. Du hast sie nie bearbeitet. Du hast sie nicht einmal geöffnet. Zerstört hat sie der Assistent, den du dir geholt hast — sein write_file behandelte die Log-Datei der Datenbank wie eine gewöhnliche Textdatei.

Das ist nicht hypothetisch. Genau das ist im Hermes-Repository selbst passiert: die kanban.db eines Teams wuchs von 550 KB zu einem Feld aus U+FFFD-Ersatzzeichen, und SQLite erklärte das Image für beschädigt und nicht wiederherstellbar. PR #114220, gemergt am 17. September 2026, schließt dieses Loch. Dieser Beitrag zeigt, wie der Schaden entstand, was der Fix wirklich absichert und ob der Build, den du gerade laufen hast, ihn enthält.

Wie es dazu kam: ein Textwerkzeug schrieb in eine Binärdatei

write_file und patch sind Werkzeuge für Text. Beim Lesen dekodieren sie eine Datei in einen String, beim Schreiben kodieren sie ihn zurück. Bei reinem Text ist dieser Round-Trip verlustfrei. Bei binärem Inhalt ist er katastrophal: Bytes, die kein gültiges UTF-8 sind, werden beim Einlesen zu U+FFFD („�“), und beim Zurückschreiben sind diese Bytes für immer verloren.

Wirklich beunruhigend daran ist, dass Hermes nie beschlossen hat, eine Datenbank zu schreiben. Es wollte irgendeine benachbarte Datei schreiben oder überschrieb beim Aufräumen eines Verzeichnisses etwas, das es für eine Textdatei hielt. Entscheidend ist die Frage, wie ein Werkzeug zu dem Urteil „das ist binär, da fasse ich nichts an“ kommt:

tools/binary_extensions.py

Die alte Prüfung sah nur auf das letzte Pfadsuffix:
  app.db        -> .db        -> in der Binärliste    -> verweigert  OK
  state.db-wal  -> .db-wal    -> nicht in der Liste   -> als Text behandelt

.db selbst war bereits durch den Overwrite-Schutz abgedeckt. Die Sidecar-Dateien waren es nicht. Was sind die also?

  • x.db-wal — das Write-Ahead-Log, der Notizzettel des WAL-Modus. Neue Schreibvorgänge landen zuerst hier und werden später in die Hauptdatei zurückgeführt. Beschädige es, und du hast das Journal zerrissen, das noch nicht gebucht wurde.
  • x.db-shm — der Shared-Memory-Index für das WAL, über den Prozesse sich auf Lese- und Schreibpositionen einigen.
  • x.db-journal — das Rollback-Journal, mit dem im Nicht-WAL-Modus eine unfertige Transaktion rückgängig gemacht wird.

Anders gesagt: die Hauptdatenbank ist ein gebundenes Journal, und das WAL ist der Stapel noch nicht gebuchter Belege auf dem Schreibtisch. Übermale die Belege, und am nächsten Morgen stimmt die ganze Buchhaltung nicht mehr. Deshalb fehlten der betroffenen Datenbank nicht „ein paar Zeilen“ — sie wurde rundheraus für beschädigt erklärt.

Was der Fix tut: ein Urteil, das sich alle Lese- und Schreibpfade teilen

Der Ansatz in PR #114220 ist sauber: nicht das Symptom flicken, sondern „das ist ein SQLite-Sidecar“ zu einem einzigen gemeinsamen Urteil machen, damit Lesen und Schreiben dasselbe Lineal benutzen.

Konkret besteht die Änderung aus:

  • Die Sidecar-Erkennung ist nach tools/binary_extensions.py gewandert — ein kleiner _lower_suffix-Helfer plus eine is_sqlite_sidecar-Prüfung — und sowohl der Schutz in read_file als auch file_tools.py und die fünf Sandbox-Lesestellen in tools/file_operations.py fragen sie nun ab. Lese-Schutz, Schreib-Schutz und die Sandbox-Lesevorgänge können sich nicht mehr widersprechen.
  • Ein Sidecar-Pfad wird auch dann verweigert, wenn die Datei noch nicht existiert. Nach einem Checkpoint kann eine Datenbank überhaupt keine Sidecars auf der Platte haben, aber der aller nächste Schreibvorgang erzeugt eines. Ein Schutz, der nur nach einer vorhandenen Datei sucht, würde genau durch dieses Fenster schlüpfen.
  • Das Überschreiben einer bestehenden Binärdatei wird weiterhin verweigert, während das Anlegen einer brandneuen .db-Datei wieder erlaubt ist — im Repo liegen Text-Fixtures, die buchstäblich *.db heißen, und ein pauschales Anlegeverbot würde sie brechen. (Das weicht bewusst von der Politik des ursprünglichen Contributors ab; das Upstream-Review hat sie gelockert.)
  • read_file liefert für ein Sidecar keinen verlustbehafteten Text mehr zurück; es wird als binär behandelt.

Upstream hat das Verhalten an einer echten Datenbank gemessen:

Aktion Vorher Nachher
write_file in state.db-wal (10 Bytes) Schreibvorgang erfolgreich, danach meldete SQLite disk I/O error verweigert, Bytes unverändert, integrity_check läuft durch
write_file einer neuen notes.db erlaubt weiterhin erlaubt
read_file eines Sidecars verlustbehafteter Text zurückgegeben als binär behandelt

21 Testfälle decken die vier Kombinationen ab (vorhandenes Sidecar / fehlendes Sidecar / die .db selbst / das patch-Werkzeug) und laufen alle durch. Auch ein Stück Prozess ist hier lehrreich: das ist eine Rettungsaktion des Community-PR #114091. Der ursprüngliche Autor hat den PR geöffnet, das Upstream-Review entschied sich gegen einen Merge in dieser Form, also wurden die Commits mit erhaltener Autorschaft per Cherry-Pick übernommen und verbreitert. Dieses Muster findet sich überall im Hermes-Repo, und es ist ein großer Teil davon, wie an einem einzigen Tag Dutzende Fixes landen können.

Hat deine Version den Fix?

Ehrlich gesagt: nein. Der Commit ist am 17. September 2026 in main gemergt worden, und das neueste getaggte Release ist v0.21.3 (Tag v2026.9.14, 14. September) — dieser Fix kommt danach. Prüfe, was bei dir läuft:

hermes --version
hermes update

Bis der Schutz in deinem Build ankommt, drei praktische Gewohnheiten:

  1. Halte laufende Datenbanken aus dem Arbeitsverzeichnis des Agenten. Verschiebe Datenbankdateien, Backup-Verzeichnisse und *-wal-Sidecars aus dem Projektbaum — oder zumindest aus den Pfaden, in die der Agent frei schreiben darf.
  2. Ändere eine Datenbank über die sqlite3 CLI, nicht über Textwerkzeuge. Sichere Operationen wie .backup (Hot-Backup) und VACUUM INTO 'copy.db' (eine kompakte Kopie) sind die Datenbank, die ihre eigenen Bytes verwaltet; es gibt keinen Text-Round-Trip, in dem Information verloren gehen kann.
  3. Sichere regelmäßig. Eine so beschädigte Datenbank ist praktisch nicht wiederherstellbar — die 550 KB große kanban.db aus dem ursprünglichen Bericht war ein Totalverlust, weil der Schaden im Read-Replace-Write-Schritt entstand: zerstört wurden die Daten selbst, nicht ein Index.

Die allgemeine Lehre hinter dem Schutz

Tritt einen Schritt zurück, und die Regel gilt für jede Agent-Toolchain: ein Textwerkzeug kann binären Inhalt nicht verlustfrei durchreichen, deshalb ist bereits das Annehmen des Ziels der Defekt — nicht etwas, das während des Schreibens schiefgeht. Die richtige Reparatur ist keine klügere Konvertierung beim Schreiben, sondern die Verweigerung des Ziels am Eingang. Hermes hat auf vier Lesepfaden schon Binärdaten erkannt; die Schreibpfade bekamen einfach nie dieselbe Behandlung. Solche Lücken leben in genau dieser Naht, wo ein Pfad nicht mitgezogen wurde.

Ein bekanntes Follow-up hat es nicht in diesen PR geschafft: Magic-Byte-Erkennung für Datenbanken ohne Endung (eine SQLite-Datei beginnt mit SQLite format 3\0). Eine Datei namens backup ohne Endung wird heute noch als Text behandelt. Wenn das zu deinen Namensgewohnheiten passt, ist Gewohnheit 1 oben — halte sie aus dem Arbeitsverzeichnis heraus — deine einzige Versicherung.

Es gehört außerdem zu einer Familie von Problemen, die wir schon behandelt haben: gleichzeitige Schreiber in dieselbe Datenbank, WAL-Handles, die ein anderer Prozess hält, und hermes doctor --fix, das auf eine Datenbank einwirkt, die es hätte in Ruhe lassen sollen. Sie alle laufen auf dieselbe Schlussfolgerung hinaus: je fähiger der Agent, desto mehr brauchen die Dinge, die er nicht anfassen darf, maschinenlesbaren Schutz, statt darauf zu vertrauen, dass du daran denkst, ihn zu warnen. Eine Verweigerung in der Werkzeugebene ist besser als eine Zeile im Prompt, die höflich darum bittet.

Zusammenfassung

  • Der Defekt: binary_extensions.py verglich nur das letzte Suffix, also wurde .db-wal als gewöhnlicher Text gelesen — in Mojibake dekodiert und zurückgeschrieben, was laufende Datenbanken zerstörte.
  • Der Fix (PR #114220, gemergt am 17. September 2026): ein gemeinsames is_sqlite_sidecar-Urteil für Lesepfade, Schreibpfade und Sandbox-Pfade; Sidecars werden verweigert, ob sie existieren oder nicht; Binär-Overwrites werden abgelehnt, während das Anlegen erlaubt bleibt.
  • Status: auf main, noch in keinem getaggten Release (das neueste ist v0.21.3).
  • Was du heute tun kannst: laufende Datenbanken auslagern, sie mit der sqlite3 CLI bearbeiten, sie sichern.

Weiterlesen: die Release Notes zu v0.21.3 behandeln dieselbe Familie von Arbeiten zur Schreibsicherheit von Datenbanken; wenn deine Datenbank bereits beschädigt ist und gerettet werden muss, hilft die Anleitung zur Wiederherstellung einer beschädigten state.db; wann der Rat von hermes doctor zu einem WAL von hilfreich zu schädlich kippt, steht in der ehrlichen WAL-Warnung; und die Container-Variante desselben Themas ist Docker und state.db-WAL über VMs hinweg.