Dein Agent schrieb um 2 Uhr nachts in deine echte crontab: HOME und cron sandboxen


Du übergibst einem Agenten eine ganz gewöhnliche Eval-Aufgabe: „Schreib eine backup.sh und plane sie so, dass sie jede Nacht um 2 Uhr läuft.“ Am nächsten Tag schaust du auf die Maschine, und crontab -l listet drei Jobs, die du nie geschrieben hast, ~/.hermes/scripts/ enthält zwei Skripte, und die Verzeichnisse, auf die sie zeigen, wurden längst gelöscht – also schlagen sie jede Nacht um 2 Uhr fehl, still und leise, monatelang.

Kaputtgegangen ist nicht die Kompetenz des Agenten, sondern eine allzu enge Auslegung des Wortes „Sandbox“. Das Harness isolierte HERMES_HOME, nahm an, Hermes’ eigener Zustand sei eingedämmt, und ließ die Terminal-Kindprozesse als der Nutzer des Operators laufen – sie schrieben in das echte ~, planten in der echten crontab und berührten den echten systemd-User-Manager.

Die drei Ausgänge, die leckten

Alles, was Hermes besitzt, lebt unter HERMES_HOME: die Session-Datenbank, die Konfiguration, Skills, Memory. HERMES_HOME auf ein temporäres Verzeichnis zu zeigen garantiert tatsächlich „sauber, wenn der Lauf endet“.

Im Terminal ausgeführte Befehle sind eine andere Sache. Wenn der Agent bash -c '...' ausführt, erbt dieses Kind die Host-Umgebung:

Ausgang Folge
HOME zeigt auf das echte Home write_file("~/.hermes/scripts/x.sh"), Anhängen an ~/.bashrc, Berühren von ~/.ssh/... — alles trifft die echte Maschine
Der Systembefehl crontab crontab - schreibt den tatsächlichen Zeitplan des Operators
DBUS_SESSION_BUS_ADDRESS / XDG_RUNTIME_DIR systemctl --user erreicht den echten User-Manager und registriert echte User-Services

Setzt man das zusammen, ergibt sich das absurde Ergebnis: ein abgeschlossener Eval-Lauf, der für immer geplante Jobs auslöst. Der Fix-PR (#132314) dokumentiert die Szene auf einer Dev-Box: drei nächtliche Cronjobs, zwei Skripte im echten ~/.hermes/scripts/ und 24 leere /tmp/ab-*-Laufverzeichnisse.

Das Produkt hatte den Vertrag schon: TERMINAL_HOME_MODE

Das Interessante am Fix ist, dass er keine Isolationsschicht erfindet – er nutzt eine, die Hermes bereits ausliefert. TERMINAL_HOME_MODE entscheidet, welches HOME Terminal-Kinder erhalten:

Wert Bedeutung
auto (Standard) Echtes HOME auf Hosts; {HERMES_HOME}/home in Containern
real Immer das echte HOME
profile HOME immer auf {HERMES_HOME}/home festnageln

(isolated und profile_home werden als Aliase akzeptiert und zu profile normalisiert.)

Der Quellcode lohnt sich allein wegen der Rangfolge: get_subprocess_home() implementiert diese Entscheidungen, und apply_subprocess_home_env() ist, wo die Arbeit passiert – es schreibt HOME auf das Profil-Home um, zeichnet das echte Home in HERMES_REAL_HOME auf und lenkt die Temp-Verzeichnis-Variablen auf ein Scratch-Verzeichnis unter HERMES_HOME um. Eine einzige Umgebungsvariable, und sowohl der Pfad „eine Datei schreiben“ als auch der Pfad „eine temporäre Datei schreiben“ landen im Profil.

Ein begleitendes Detail gibt es auf der Seite des Write-Guards: Wenn entschieden wird, ob ein Pfad ein geschütztes Home ist, nimmt der Guard die Vereinigung aus Prozess-Home, echtem Home, Subprozess-Home und Profil-Home. Sonst bekäme man den perversen Fall, in dem HOME auf das Profil festgenagelt ist, aber ein absoluter Schreibvorgang ins echte Home ungeschützt bleibt.

Was die Eval-Sandbox tatsächlich tut

Mit dem verstandenen Vertrag lesen sich die rund 40 Zeilen Sandbox-Code schlicht. Bevor irgendein Agent-Code läuft, tut sie vier Dinge:

# evals/core_tool_deferral/sandbox.py (Auszug)
os.environ["HOME"] = home                      # 1. das eigene ~ des Prozesses zeigt auf das Profil-Home
os.environ["TERMINAL_HOME_MODE"] = "profile"   # 2. Terminal-Kinder folgen
os.environ["PATH"] = bin_dir + os.pathsep + os.environ.get("PATH", "")  # 3. crontab-Shim zuerst
for var in ("DBUS_SESSION_BUS_ADDRESS", "XDG_RUNTIME_DIR"):
    os.environ.pop(var, None)                  # 4. den systemd-User-Bus trennen

Das crontab in Schritt 3 ist ein ~10-zeiliges Shell-Skript, das die echte CLI-Oberfläche implementiert – -l, -r, -e, Lesen aus stdin oder einer Datei – während es alles in einer einfachen Datei im Laufverzeichnis speichert. Der Agent bekommt identisches Feedback (crontab -l zeigt die Jobs, die er geplant hat) und der echte Zeitplan des Operators wird kein einziges Byte berührt. Das ist die am besten kopierbare Idee hier: nicht isolieren, indem man verweigert; isolieren, indem man etwas einsetzt, das sich gleich verhält.

Schritt 1 sieht redundant aus und ist es nicht. Das HOME des Terminal-Kindes umzuschreiben reicht nicht, weil das write_file-Tool, das dem Modell gereicht wird, ~ im Prozess expandiert – also muss auch das eigene HOME des Prozesses wandern, sonst landet write_file("~/.hermes/scripts/x.sh") trotzdem auf der echten Maschine.

Was jene 24 /tmp/ab-*-Verzeichnisse angeht: Es war keine Code-Änderung nötig. Hermes exportiert jetzt TMPDIR für jeden Prozess, das auf sein eigenes Scratch-Verzeichnis zeigt, sodass ein einfaches mkdtemp() bereits dort landet, wo es soll; die Überbleibsel stammen von vor dieser Änderung.

Was das für dein Setup bedeutet

Wenn du einen einzelnen Agenten auf deinem eigenen Laptop laufen lässt, wirst du TERMINAL_HOME_MODE wahrscheinlich nie anfassen – dass auto auf Hosts das echte HOME beibehält, ist genau das, was du willst, da deine Skripte in dein eigenes Home-Verzeichnis schreiben sollen.

Drei Szenarien brauchen die Einstellung wirklich:

  1. Evals und Batch-Läufe. Du bittest den Agenten, ein Skript zu schreiben und es zu planen, und Dutzende Läufe dürfen keinen Maschinenzustand gemeinsam nutzen. TERMINAL_HOME_MODE=profile (plus crontab-Shim und User-Bus-Trennung) ist der Boden, nicht die Decke.
  2. Multi-Tenant- oder geteilte Maschinen. Dass der Agent eines anderen Profils in dein Home-Verzeichnis schreibt, ist nicht hypothetisch. Setz über diese Variable hinaus das Terminal auf ein isoliertes Backend (Docker, SSH), damit der Agent gar nicht erst auf das Dateisystem des Operators zugreifen kann.
  3. Unbeaufsichtigte Cronjobs. Ein vom Gateway gestarteter Job, der crontab berührt, schreibt auch den echten Zeitplan. Dämme zuerst sein HOME und PATH ein.

Ein praktischer Selbsttest: Gib dem Agenten eine Nur-Lese-Aufgabe – „liste die aktuell geplanten Jobs auf“ – und schau, ob er deine tatsächliche Tabelle oder ein leeres Laufverzeichnis liest. Das sagt dir mehr über deine Sandbox als jedes Dokument.

Wann du es bekommst

Der Sandbox-Fix wurde am 3. Oktober 2026 in den main-Branch gemergt. Das neueste Release ist v0.21.5 (Tag v2026.9.24), also ist die Änderung selbst nur auf main – während der TERMINAL_HOME_MODE-Vertrag bereits in den veröffentlichten Versionen existiert und heute nutzbar ist.

Für mehr Tiefe behandelt der Artikel zu Sandbox und Media-Delivery die andere Seite der Grenze; der Beitrag zum Datenverlust durch ein schmutziges Worktree ist die Falle, in die du läufst, wenn der Agent dein Repository berührt; und der Leitfaden zum Terminal-Backend-Plugin zeigt, wie du das Terminal vollständig auf ein anderes Backend verlagerst.

Die Kurzfassung

Die Lektion verallgemeinert sich weit über diesen Fix hinaus: Zähle Ausgänge, nicht Verzeichnisse. HERMES_HOME ist Hermes’ eigener Zustand; das Home-Verzeichnis, crontab, der User-Bus und das Temp-Verzeichnis sind vier Betriebssystem-Ausgänge, und einen davon zu verpassen reicht, um zu lecken. Wenn du Eval-Harnesses schreibst oder Multi-Tenant-Setups betreibst, ist TERMINAL_HOME_MODE=profile plus ein verhaltenskompatibles crontab-Shim das wertvollste Veränderungspaar, das du machen kannst.