/rollback meldete Erfolg — und verschlang still die nicht committeten Änderungen im verschachtelten Repo

Es sah nach deinem sichersten Undo aller Zeiten aus. Ein Refactoring ist schiefgegangen, also hast du /rollback list geprüft, einen checkpoint von vor zwei Stunden ausgewählt und die Nummer eingetippt. Das Terminal meldete Erfolg. Dann wechselst du per cd in ein Unterverzeichnis, um weiterzumachen, und die nicht committeten Änderungen im dort liegenden Repository sind einfach weg — nicht im Papierkorb, nicht im stash.
Das ist Issue #127073. checkpoints machen zwar einen Snapshot deines Arbeitsverzeichnisses, aber wenn dieses Verzeichnis ein weiteres git-Repository enthält, wird das innere Repository als gitlink festgehalten — als reine commit-Referenz — und seine Dateien gelangen nie in den Snapshot. Der rollback meldete trotzdem Erfolg, also hast du die Arbeit verloren und ein falsches Alles-klar bekommen. Der Fix (PR #127839, der #127077 von @dskwe rettet) ist am 2026-09-29 auf main gelandet.
Was checkpoints speichern — und was sie nie gespeichert haben
Die Mechanik entscheidet, wann es dich erwischt. Vor einem write_file-, patch- oder terminal-Aufruf committet Hermes einen Snapshot des Arbeitsverzeichnisses in ein gemeinsames Bare-Repository:
~/.hermes/checkpoints/
├── store/ # one shared bare repo
│ ├── refs/hermes/<hash> # per-project branch tip
│ ├── indexes/<hash> # per-project git index
│ └── projects/<hash>.json
Die Nutzung von gits eigenem Objektmodell hält Snapshots billig und diffbar — und genau daraus entsteht die Lücke: git speichert den Inhalt eines Unter-Repositories nicht im übergeordneten Repository. Der Baum des Eltern-Repos enthält nur einen gitlink, der auf einen einzigen commit zeigt. Änderungen, die du im inneren Repo nicht committet hast, existieren aus Sicht des äußeren Repositories schlicht nicht.
Das ergibt die denkbar schlechteste Kombination:
- Dateien, die der Agent im äußeren Baum geändert hat, werden aus dem checkpoint wiederhergestellt ✅
- dieselbe Operation setzt das innere Repository auf den commit zurück, den sein gitlink benennt, und verwirft nicht committete Inhalte ❌
- und der Befehl meldet Erfolg ✅ ← das ist der eigentlich gefährliche Teil
Nach dem Fix: vorab ablehnen statt hinterher um Entschuldigung bitten
Die neue Regel ist unmissverständlich: vorher entscheiden, bevor irgendetwas angefasst wird, und wenn der rollback einen gitlink erreichen würde, gar nichts ändern.
Das Verhalten Punkt für Punkt:
- Ein vollständiger rollback (
/rollback <N>) wird abgelehnt, weil das Wiederherstellen von allem zwangsläufig den gitlink einschließt. - Jede Auswahl, die einen gitlink berührt, wird ebenfalls abgelehnt — der gitlink selbst, eine Datei darunter oder ein übergeordnetes Verzeichnis, das ihn enthält. Die Ablehnung erfolgt, bevor irgendeine Datei, ein Index, eine ref oder ein Sicherheits-Snapshot vor dem rollback geändert wird, und die Fehlermeldung benennt die Repositories, die nicht erfasst wurden.
- Auswahlen, die gitlinks meiden, stellen weiterhin wieder her, einschließlich Git-Pathspecs wie
*.txt,:(glob)…,:(exclude)tool. Die Implementierung löst die Auswahl zuerst gegen einen Wegwerf-Index auf: Ein Wildcard, der einen gitlink erreichen würde, wird abgelehnt; einer, der das nicht täte, verhält sich genau wie vorher. Das ist wichtig — der frühere Anlauf zu diesem Fix hat jeden pathspec pauschal zurückgewiesen und damit die weiterhin funktionierende*.txt-Wiederherstellung entfernt. - Die Schutzprüfung verwendet den im checkpoint festgehaltenen Pfad, nicht das heutige Dateisystem. Früher ließ sich eine erfasste
notes.txt, die später zu einem Symlink in ein verschachteltes Repo wurde, nicht wiederherstellen; jetzt geht es. Umgekehrt wird ein festgehaltener gitlink, der inzwischen durch einen unabhängigen Symlink ersetzt wurde, weiterhin abgelehnt. Die Prüfung der Pfad-Eingrenzung (_validate_file_path) ist unverändert.
Mehr als eine Ablehnung: checkpoints sagen es jetzt laut
Eine Ablehnung hilft bei dem, was bereits passiert ist, nicht weiter, deshalb fügt der Patch zusätzlich eine Offenlegung zum Zeitpunkt der Erfassung hinzu. Jeder checkpoint, der einen gitlink gespeichert hat, bekommt eine Annotation an seine eigene Nachricht angehängt:
before write_file: app.py [nested git repos not captured: tool]
Bei mehr als drei Pfaden bekommst du die ersten drei plus +N, zum Beispiel [nested git repos not captured: a, b, c (+2)]. Da die Auflistungen von /rollback ohnehin die Beschreibung eines checkpoints rendern, siehst du die Warnung schon beim Auswählen eines checkpoints — nicht erst nach einem fehlgeschlagenen rollback.
Der gleiche Patch hat einen Bug behoben, der die Annotation verbarg: Die rollback.list-Oberfläche in TUI und Desktop las einen message-Key, während der Manager ihn reason nennt — daher zeigten diese Auflistungen überhaupt keine checkpoint-Beschreibungen an. Jetzt ist das gemappt, was sowohl die alten Beschreibungen als auch das neue Label sichtbar macht.
Eine plattformübergreifende Kleinigkeit: Pfadkomponenten werden jetzt mit der plattformeigenen Path getrennt, sodass packages/nested und packages\nested unter Windows beide korrekt abgelehnt werden — der Befund zur Pfadnormalisierung, der im Review der früheren PR aufkam.
Was du damit anfängst
Sobald die Mechanik klar ist, ist der praktische Rat kurz, und jeder Punkt spart Stunden:
- Committe im Sub-Repo, bevor ein Agent das äußere anfasst. Ein gitlink zeigt auf einen commit; nur committete Inhalte werden referenziert. Wenn die Arbeit noch nicht in die Hauptlinie soll, sind
git stashoder ein Wegwerf-commit besser, als darauf zu hoffen, dass der checkpoint sie abdeckt. - Lies die Liste, bevor du zurückrollst.
/rollback listzeigt jetzt[nested git repos not captured: …]. Siehst du diese Annotation, kann dieser checkpoint nur äußere Dateien wiederherstellen — um das innere Repository musst du dich selbst kümmern. - Nutze pathspecs für chirurgische Wiederherstellungen.
/rollback 1 src/broken_file.pyist weit sicherer als ein rollback über ein ganzes Verzeichnis, und Wildcards werden wieder unterstützt, solange sie keinen gitlink berühren. - Verlass dich bei Container-Backends nicht darauf. Mit
docker,singularity,modal,daytonaoder einem Container-Plugin gehören Pfade zur Sandbox statt zum Host: Hermes nimmt für sie keine checkpoints auf, und/rollbacklistet bestehende Host-checkpoints auf, verweigert aber diff und restore — sowohl in der CLI als auch in Messaging-Chats./diff sessionnennt denselben Grund. Lokale und SSH-Backends sind nicht betroffen.
Vier bestehende Schutzgeländer solltest du im Kopf behalten, wenn aus einer Achterbahnfahrt ein „warum kann ich nichts wiederherstellen“ wird: Verzeichnisse mit mehr als 50.000 Dateien werden übersprungen, eine einzelne Datei über max_file_size_mb (Standard 10 MB) bleibt außerhalb des Snapshots, der Store verwirft im Round-Robin den ältesten commit pro Projekt, sobald er max_total_size_mb (Standard 500 MB) überschreitet, und checkpoints werden transparent deaktiviert, wenn git nicht im PATH ist.
Um zu sehen, wie viel Platz die Historie belegt, oder um sie zu löschen:
hermes checkpoints status # Gesamtgröße, Anzahl der Projekte, Aufschlüsselung pro Projekt
hermes checkpoints prune # verwaiste/veraltete checkpoints entfernen und den Store GCen
hermes checkpoints clear # die gesamte Basis löschen (die ganze /rollback-Historie)
Eine Voraussetzung zuerst: checkpoints sind opt-in. Seit v2 sind sie standardmäßig aus — aktiviere sie für eine Sitzung mit hermes chat --checkpoints oder global mit checkpoints.enabled: true in ~/.hermes/config.yaml. Einmal an, hält jedes Projekt standardmäßig bis zu max_snapshots: 20 checkpoints, und die automatische Wartung läuft höchstens einmal alle 24 Stunden und räumt Projekte weg, die 7 Tage nicht berührt wurden. Wer sie nie eingeschaltet hat, sieht in /rollback einfach eine leere Liste — das ist kein Bug.
Halt das mentale Modell sauber: Ein checkpoint ist ein Undo für das, was der Agent gerade getan hat, kein Backup jeder Datei, die in deinem Baum existiert. Dieselbe Art von Überraschung hatten wir in dem Beitrag zur stillen Dateikorruption: Einem Tool zu vertrauen, dass es für dich zurücksetzt, funktioniert nur, wenn du genau weißt, was es gespeichert hat.
Die Kurzfassung
-
Ein verschachteltes git-Repository erscheint im checkpoint als gitlink; seine nicht committeten Änderungen waren nie im Snapshot.
-
Vor dem Fix hat
/rollbackdas innere Repo überschrieben und Erfolg gemeldet — die schlechteste Kombination. -
Jetzt werden ein vollständiger rollback und jede Auswahl, die einen gitlink berührt (ihn selbst, eine Datei darunter oder sein übergeordnetes Verzeichnis), abgelehnt, bevor sich etwas ändert, und die Fehlermeldung nennt die nicht erfassten Repositories.
-
Auswahlen, die gitlinks meiden, stellen weiterhin wieder her, inklusive Git-Pathspecs, und die Behandlung von Symlinks und Windows-Pfaden folgt dem festgehaltenen Pfad.
-
checkpoints, die ein verschachteltes Repo verpasst haben, werden zum Zeitpunkt der Erfassung mit
[nested git repos not captured: …]gekennzeichnet — sichtbar in/rollback list, der TUI und dem Desktop. -
Praktisch: erst im Sub-Repo committen oder stashen,
/rollback <N> <path>bevorzugen und daran denken, dass Container-Backends keine Host-checkpoints haben. -
checkpoints sind seit v2 opt-in (
hermes chat --checkpointsodercheckpoints.enabled: true) — eine leere/rollback-Liste heißt meist nur, dass der Store nie eingeschaltet wurde.
Die vollständige Flag-Referenz findest du unter hermes checkpoints; zur benachbarten Integritätsarbeit siehe diese Runde von Compaction-Fixes.