Das Beenden einer Session löschte nie committeten Code


Drei Sessions laufen parallel. Eine davon ist mitten in einer Dateibearbeitung — noch kein Commit, der Rest soll morgen fertig werden. Die Session wird beendet, das Terminal geschlossen. Am nächsten Morgen ist das Verzeichnis unter .worktrees/ weg, und der Branch ebenfalls.

Das war keine falsche Erinnerung an den Ort. Der Exit-Cleanup für worktree: true übersieht genau eine Prüfung: Er schaut auf Commits, nie auf Dateien.

Was der Worktree-Modus tut

Eine Session mit hermes -w zu starten (oder worktree: true in der Config zu setzen) gibt jeder CLI-Session ihren eigenen Worktree unter .worktrees/, auf einem eigenen Branch:

worktree: false        # Standard: nur wenn -w übergeben wird
worktree_sync: true    # Standard: vom frisch gefetchten Remote-Tip abzweigen; false nutzt lokales HEAD

So können mehrere Agents bearbeiten, committen, pushen und PRs öffnen, ohne sich gegenseitig auf die Füße zu treten. Auch das Abzweigen vom frischen Remote-Tip ist Absicht — der PR bleibt auf die eigene Änderung beschränkt, statt den Rückstand des lokalen Clones zu erben.

Das Problem sitzt im Exit-Schritt.

Was die Doku verspricht und was der Code tut

Die Konfigurationsdoku ist im Abschnitt zur Git-Worktree-Isolation eindeutig:

Saubere Worktrees werden beim Beenden entfernt; dirty Worktrees bleiben für die manuelle Wiederherstellung erhalten.

_cleanup_worktree in cli.py nennt im Docstring dagegen ein eigenes Kriterium:

def _cleanup_worktree(info: Dict[str, str] = None) -> None:
    """Remove a worktree and its branch on exit; kept only when it has unpushed commits."""

Nur nicht gepushte Commits bleiben erhalten. Der entscheidende Helper, _worktree_has_unpushed_commits(), führt aus:

git log --oneline HEAD --not --remotes

Mit anderen Worten: Er fragt, welche Commits auf HEAD kein Remote hat. Der Status des Arbeitsbaums wird nie gelesen. Also:

Was der Worktree beim Beenden enthält Tatsächliches Ergebnis
Eine neue untracked Datei, nie committet Worktree GELÖSCHT
Eine getrackte Datei geändert, nicht committet Worktree GELÖSCHT
Ein Commit, noch nicht gepusht Worktree bleibt erhalten

Der letzte Schritt ist git worktree remove <path> --force plus git branch -D <branch>. --force bedeutet genau das, wonach es klingt: nicht committete Änderungen im Arbeitsbaum ignorieren.

Warum es zwei verschiedene Kriterien gibt

Hermes hat tatsächlich zwei Pfade, die Worktrees verwalten, und sie sind sich uneinig.

Der eine ist der oben beschriebene Exit-Cleanup in cli.py — nur Commits.

Der andere ist der Startup-Pruner in hermes_cli/worktree_ops.py, und der versteht „dirty“: _worktree_is_dirty() führt git status --porcelain aus (untracked Dateien inklusive), scheitert sicher in Richtung True, und sein Urteil wird als dirty festgehalten, ein Mitglied von _PRESERVE_REASONS. Der Kommentar darüber ist ungewöhnlich deutlich:

Guards auf jeder Stufe und in jedem Alter: dirty Trees werden nie entfernt.

Das ist also nicht „Hermes kann dirty nicht von clean unterscheiden“. Es sind zwei Pfade mit zwei verschiedenen Kriterien: Der Hintergrund-Prüfer schützt dirty Dateien, der Exit-Cleanup schützt Commits. Nutzer treffen auf den Exit-Pfad.

Wie man sich schützt

Bis dieses Kriterium angeglichen ist, hilft heute Folgendes.

  1. Vor dem Beenden committen, notfalls als WIP. Ein nicht gepushter Commit lässt den Tree überleben:
    git add -A && git commit -m "wip: still working"
  2. Oder stashen, inklusive untracked Dateien. Das -u in git stash -u ist der wichtige Teil — ohne es existieren neue Dateien weiterhin nur innerhalb des Worktrees.
  3. „Die Session ist noch offen“ ist kein Dateischutz. Nicht committete Arbeit ist nur sicher, solange die Session lebt; im Moment des Beendens gerät sie in Reichweite von --force.
  4. Bei parallelen Sessions einen Blick auf den Status werfen. git -C .worktrees/<name> status --porcelain; eine nicht leere Ausgabe bedeutet, dass dieser Tree beim Beenden gelöscht wird.
  5. Die Session committen und pushen lassen. Worktree-Sessions sind darauf ausgelegt, den Agent committen, pushen und PRs öffnen zu lassen; einmal gepusht, hält das Kriterium „nicht gepushte Commits“ den Tree am Leben, und es kommt ein zweites Sicherheitsnetz hinzu.
  6. Regelmäßig auditieren. Es lohnt zu wissen, was der in v0.20.5 eingeführte Worktree-Check-up abdeckt und was nicht.

Wenn es schon passiert ist: Zuerst mit git worktree list und git branch prüfen, ob der Branch überlebt hat. Gab es vor dem Beenden auch nur einen Commit, liegt dieser Inhalt noch im Object Store und ist über den Branch oder das Reflog wiederherstellbar. Doch nicht committete, untracked Dateien, die mit --force entfernt wurden, haben keinen Recovery-Pfad auf Git-Ebene — ob sie zurückkommen, hängt von Editor-Puffern, System-Snapshots oder Backups ab, nicht von Git. Besser, das deutlich zu sagen, als eine Rettung anzudeuten, die es nicht gibt.

Aktueller Stand

Issue #129482 wurde am 2026-09-30 gegen den Code bei 41755854 angelegt, als P1 markiert und war zum Zeitpunkt dieses Textes noch offen. Der naheliegende Fix ist, dass der Exit-Cleanup ebenfalls _worktree_is_dirty() aufruft, sodass beide Pfade ein Kriterium teilen. Bis dahin helfen die Schritte 1 und 2 oben.

Das ist auch nicht der erste dieser Bugs, bei denen etwas „sauber aussieht und still etwas verloren geht“. Der Nested-Repo-Rollback-Guard behandelt Rollbacks, die Verzeichnisse berühren, die nie erfasst wurden, und Session-Grenzen erklärt, was Sessions tatsächlich teilen. Beide lohnen sich als fester Bestandteil der eigenen Routine.

Das Fazit

Die Lehre lautet nicht „Worktrees nicht verwenden“. Sie lautet, dass eine Löschoperation das konservativste verfügbare Kriterium nutzen sollte: Bei der Entscheidung, ob ein Tree entfernt werden darf, zählt der Inhalt des Arbeitsverzeichnisses weit mehr als der des Commit-Graphen. Eine nicht committete, halbfertige Datei ist meist mehr wert als zehn gepushte Commits.

Bis der Code sich einig wird, besteht Selbstverteidigung aus einem Satz: vor dem Beenden committen oder stashen.