Ein einziges Update riss ein 434-GB-Loch in die Festplatte


Ein Klick auf Update am Laptop, kurz Kaffee holen, und zurück kommt man zu einem Browser, der sich nicht mehr öffnen lässt, einem Editor, der über fehlenden Speicherplatz klagt, und einem Hermes, das den Start verweigert. Der Übeltäter: ~/.hermes/hermes-agent/.git, gewachsen von rund einem Gigabyte auf 434 GB, was eine 1-TB-SSD aufgefressen hat.

Das war nicht „der Update-Download war riesig“. Ein Hintergrund-Versionscheck hat bei einem bestimmten Installations-Layout das On-Demand-Fetching ausgelöst, und daraus wurde eine Lawine. Hier steht der Mechanismus, die echten Zahlen und wie aufgeräumt wird.

Ein Git-Aufruf, der nur nach einem Pfad fragt

Die Hermes-Desktop-App führt beim Start eine Konsistenzprüfung aus (intern: bundle-skew). Sie fragt, ob der in die App gebündelte Code mit dem Code auf der Platte übereinstimmt, und bittet um einen Neustart, wenn sie auseinanderlaufen. Dafür verwendet sie zwei Git-Befehle:

git merge-base --is-ancestor <stamp> HEAD
git rev-list --count <stamp>..HEAD -- apps/desktop/...

In einem normalen Clone sind beide billig. Das Problem ist das Installations-Layout: Manche Installationen sind treeless partial clones (--filter=tree:0 — Trees werden bei Bedarf geholt, statt für jeden historischen Commit vorgehalten zu werden).

Der zweite Befehl ist pfadgefiltert (-- apps/desktop/...). Um zu zählen, wie viele Commits in diesem Bereich apps/desktop berührt haben, muss Git den Tree jedes Commits im Bereich lesen. Keiner davon liegt lokal, also fällt Git auf die Promisor-Maschinerie zurück und holt sie über das Netzwerk — und jedes Fetch hinterlässt sein eigenes Promisor-Pack auf der Platte. Längerer Verlauf bedeutet mehr zu holende Trees und mehr Packs.

Im Incident-Report umfasste dieser Bereich 1.784 Commits.

Von 1 GB auf 434 GB: die Zahlen

Alles Folgende stammt aus dem Report in Issue #129514 (macOS, 1-TB-SSD):

Posten Wert
.git-Wachstum ~1,1 GB bei einem vollen Clone → 434 GB
Promisor-Packs 2.659 (~398 GiB)
Verworfene tmp_pack_*-Dateien 1.309 (~29 GiB)
Größtes einzelnes Pack 641 MB
Gleichzeitige Hintergrund-Wartung 179 Prozesse auf dem Höhepunkt (git maintenance run --auto → git commit-graph write --split --reachable), jeder ~2 GB RSS
Systemlast Load Average 220
Prüfintervall ein neuer rev-list alle ~2 Minuten nach dem Reboot, jeder mit 9–15 Minuten Laufzeit
Ergebnis Das Update schlug mit [Errno 28] No space left on device fehl, der Mac startete neu, und CLI/TUI starteten danach nicht mehr

Frühere Schübe stehen im selben Report: 138 Promisor-Packs am 25.09., 11 am 27.09., 339 am 28.09., dazwischen ein unerwarteter Reboot. Das war kein einzelner unglücklicher Lauf — es summiert sich.

Warum „ist doch schon gefixt“ mitten im Update nicht rettet

Der Fix landete am 28.09. Heute sieht die Prüfung auf main so aus:

const options = {
  cwd: repoRoot,
  env: { ...process.env, GIT_NO_LAZY_FETCH: '1', GIT_TERMINAL_PROMPT: '0', GCM_INTERACTIVE: 'Never' },
  timeoutMs: 5000
}

GIT_NO_LAZY_FETCH=1 bedeutet Objekte nicht bei Bedarf nachladen — stattdessen fehlschlagen. Genau diese Semantik will die Prüfung, da sie sich nie auf das Netzwerk verlassen sollte. Dazu der 5-Sekunden-Timeout und das Reaping des Prozessbaums, und sie kann die Maschine nicht mehr herunterziehen.

Doch es gibt ein Timing-Problem, und es ist der wertvollste Teil des Reports:

Der Build, der die Prüfung ausführt, ist der bereits installierte, und er wird erst ganz am Ende des Updates ersetzt.

Die Desktop-App des Reporters wurde am 27.09. aus 96cce68 gebaut — einen Tag vor dem Fix. Nachdem das Update 1.292 Commits gezogen hatte, lief die Prüfung der alten App mit dem alten Code über 96cce68..HEAD, ohne GIT_NO_LAZY_FETCH. Die Platte lief voll, das Update schlug vor dem Schritt fehl, der den neuen Desktop-Build einsetzt, also blieb die ungefixte App installiert — und nahm die Schleife nach dem Reboot wieder auf. Sie heilt sich nicht selbst.

Prüfen, ob man betroffen ist

Vier schreibgeschützte Befehle, ein paar Sekunden:

du -sh ~/.hermes/hermes-agent/.git                          # Größe des Object Store; ~1 GB ist normal
ls ~/.hermes/hermes-agent/.git/objects/pack/*.pack | wc -l  # Anzahl der Packs; ein- oder zweistellig ist normal
ls ~/.hermes/hermes-agent/.git/objects/pack/tmp_pack_* | wc -l  # nicht aktivierte Downloads; normalerweise 0
df -h ~                                                     # freier Speicherplatz

Dann nach einer Schlange von Git-Prozessen und abnormer Last Ausschau halten:

uptime
ps aux | grep -E '[r]ev-list|[g]it maintenance' | wc -l

Pack-Zahlen im Hunderterbereich, ein Haufen tmp_pack_*, Last im Dutzendbereich — das ist dasselbe Muster.

Wie aufgeräumt wird

Das ist eine allgemeine Git-Aufräum-Sequenz, keine offizielle Befehlsliste. Jeder Schritt sollte verstanden sein, bevor er ausgeführt wird:

  1. Die Blutung stoppen. Die Desktop-App beenden (sonst feuert sie alle paar Minuten erneut) und die laufenden git rev-list / git maintenance-Prozesse stoppen. In der Zwischenzeit keine schwere Arbeit auf dieser Maschine starten.
  2. Die nicht aktivierten Überreste löschen. tmp_pack_*-Dateien in .git/objects/pack/ sind unvollständige Downloads, auf die Git nie verweisen wird; sie lassen sich gefahrlos entfernen. Im berichteten Fall wurden allein damit rund 29 GiB freigegeben.
  3. Den Object Store konsolidieren. git gc ausführen (bei Bedarf vorangestellt von git repack -ad), um die verstreuten Packs zusammenzuführen. Zu bedenken: gc auf einem Repo mit 2.659 Packs ist langsam und CPU-hungrig — am besten ausführen, während die Maschine ansonsten idle ist, und dabei df -h beobachten.
  4. Den Code von der Kommandozeile aus aktualisieren. Das entscheidende Detail: aus einem Terminal aktualisieren, nicht aus der Desktop-App vor dem Fix, die das Problem verursacht hat. Die Flags stehen in der hermes update-Referenz. Ist die Installation ein gefilterter/shallow Clone, der in diesem Zustand feststeckt, ist eine frische Vollinstallation aus dem offiziellen Kanal oft schneller als das Entwirren.
  5. Bestätigen, dass der Fix vorhanden ist. Nach dem Update sollte ~/.hermes/hermes-agent/apps/desktop/electron/bundle-skew.ts GIT_NO_LAZY_FETCH enthalten. Falls nicht, läuft noch der alte Code.
  6. Eine gedankliche Obergrenze für .git setzen. Rund ein Gigabyte ist gesund; zig Gigabyte bedeuten, dass etwas still Verlauf nachzieht. Nicht erst warten, bis die Platte voll ist.

Aktueller Stand

Stand 2026-10-01 ist Issue #129514 weiterhin offen und als P1 markiert. Es ist ein Follow-up zu #125932, das denselben Mechanismus in einem milderen Stadium beschrieb (Dutzende hängender Git-Prozesse). Der Fix selbst liegt auf main — bundle-skew.ts setzt jetzt GIT_NO_LAZY_FETCH=1 mit einem 5-Sekunden-Timeout, und hermes_cli/_subprocess_compat.py führt ein passendes NO_LAZY_FETCH_ENV mit —, doch der Upgrade-Pfad aus einem Build vor dem Fix hat keinen Schutz, und genau das ist das Kernproblem des Reports. Ob alte Builds eine zusätzliche Absicherung bekommen, ist nicht entschieden.

Wer aus einem partiellen Clone installiert oder Update in der Desktop-App anklickt, sollte die Schritte 1 und 2 oben heute ausführen.

Das Fazit

Keine einzige Zeile Code war hier „falsch“. Zwei vernünftige Entscheidungen sind kollidiert: ein treeless Clone, um Platte zu sparen, und eine Versions-Konsistenzprüfung für die Sicherheit — wobei diese Prüfung einen pfadgefilterten Verlaufsspaziergang enthält, der auf einem treeless Repo „lade den gesamten Verlauf herunter“ bedeutet. Als die Lawine einmal rollte, legte Git die eigene Hintergrund-Wartung obendrauf und nahm CPU, Speicher und Platte gleichzeitig mit.

Drei Signale sind es wert, im Kopf zu behalten: die Größe von .git, die Anzahl der Pack-Dateien und die Zahl der tmp_pack_*-Überreste. Alle drei sind in Sekunden ausgelesen, und alle drei hätten lange vor dem Vollaufen der Platte verraten, dass etwas nicht stimmt. Updates haben auch andere Fehlermodi — ein Update, das hängt, aber Erfolg meldet und ein Upgrade sauber durchziehen lohnen beide einen Blick.