Das terminal-Backend war gepinnt — warum lief Hermes trotzdem auf dem Host?


Du installierst Hermes auf einer gemeinsam genutzten Dev-Box. Die IT schreibt terminal.backend: docker in /etc/hermes/config.yaml, mit der Absicht, dass jeder Befehl in einem Container statt auf dem Host läuft. Eine Kollegin bittet den Agent, eine Abhängigkeit zu installieren, und sie landet trotzdem im Home-Verzeichnis des Hosts. Die Policy sieht angewendet aus. Ist sie aber nicht.

„Konfiguriert, berechtigt und trotzdem ignoriert“ ist die übelste Fehlerklasse, weil nichts einen Fehler meldet. Zwei gemergte Änderungen (PR #145221 und #145222) beheben genau das: der Terminal-Scope pro Session hat Managed Scope nie gelesen, ein gepinntes Backend verlor also bei jedem Turn gegen die eigene Einstellung des Profils.

Wer entscheidet, wo deine Befehle laufen

terminal.backend wählt das Ausführungsziel: local, docker, ssh, modal, daytona, vercel_sandbox oder ein Backend, das ein Plugin beisteuert. Der Wert wird nicht einmal beim Start gelesen. Im Gateway-Modus baut jede Session ihren eigenen Terminal-Scope zum Zeitpunkt der Erstellung, indem sie mehrere Quellen übereinanderstapelt:

  1. die eingebauten Defaults;
  2. die .env des Profils;
  3. das Launch-Overlay (Werte, die auf der Kommandozeile übergeben werden);
  4. die config.yaml des Profils.

Für einen Einzelnutzer reicht das — eigene config.yaml ändern, fertig. Aber alle vier Stufen sind vom Nutzer kontrolliert, und Managed Scope existiert genau deshalb, um darüber zu liegen.

Was Managed Scope ist

Managed Scope ist der Flotten-Mechanismus: ein systemweites Verzeichnis, das eine Reihe von Konfigurationswerten und Secrets für alle Nutzer der Maschine pinnt. Standardmäßig ist das /etc/hermes/:

/etc/hermes/
├── config.yaml     # verwaltete Config-Schicht, gewinnt gegen ~/.hermes/config.yaml
└── .env            # verwaltete Env-Schicht, gewinnt gegen ~/.hermes/.env und die Shell

Verzeichnis und Dateien gehören root (Verzeichnis 0755, Dateien 0644). Die Dateisystemrechte sind der Durchsetzungsmechanismus: Alle können sie lesen, nur ein Administrator kann sie schreiben. Der Ort lässt sich mit HERMES_MANAGED_DIR verschieben, für Container und Deployments außerhalb von /etc — und diese Variable muss vom Administrator festgelegt werden (in die Unit-Datei oder ins Image gebacken). Kann ein normaler Nutzer sie setzen, kann er Managed Scope auf ein Verzeichnis zeigen lassen, das ihm gehört, und das ganze Konstrukt fällt zusammen.

Die Rangfolge, für die Keys, die die Managed-Schicht tatsächlich angibt:

Stufe config.yaml .env
1 /etc/hermes/config.yaml (managed) /etc/hermes/.env (managed)
2 ~/.hermes/config.yaml (user) ~/.hermes/.env (user)
3 eingebaute Defaults bereits vorhandene Shell-Umgebung

Gemerged wird auf Blatt-Ebene: model.default zu pinnen friert den Rest von model.* nicht ein. Genau das macht die Schicht zu einer Baseline und nicht zu einem Schloss auf allem.

Die fehlende Schicht

load_config() — der normale Config-Pfad — wendet das Managed-Overlay an. hermes config zeigt den gepinnten Wert also korrekt. Aber der Terminal-Scope jeder Session wird von einer anderen Funktion gebaut, build_profile_terminal_scope(), und die las nur die vier Stufen oben. Nie die verwaltete. Das Ergebnis:

  • das terminal.backend: docker des Administrators ist in hermes config sichtbar;
  • die Befehlsausführung nutzt trotzdem das local des Profils.

Eine solche Trennung von Anzeige und Verhalten ist aus der Config-UI heraus nahezu unsichtbar. Aufgefallen ist sie in einem Sandbox-Plugin, das ein Single-Profile-hermes serve --isolated startet, terminal.backend über HERMES_MANAGED_DIR pinnt und die .env des Profils zurückhält: Das Plugin glaubte, Befehle liefen innerhalb seiner Grenze; sie liefen außerhalb.

Der Fix ist bewusst klein: Das verwaltete terminal:-Mapping wird zuletzt gemerged, Key für Key, mit demselben Deep-Merge, den load_config() verwendet. Drei Verhaltensweisen sollte man sich merken:

  • null pinnt nichts. docker_env: null in der Managed-Schicht lässt den Wert des Profils unangetastet.
  • Mappings mergen pro Blatt, nicht als Block. Ein verwalteter docker_env-Eintrag behält die übrigen Einträge des Profils.
  • Nur eine nicht-null docker_image gilt als gepinnt, und nur sie markiert das Image als vom Administrator gewählt.

Ohne verwalteten terminal:-Abschnitt wird der Scope exakt wie bisher gebaut — kein bestehendes Deployment ändert sein Verhalten.

Zwei .env-Fallen hinter demselben Bug

Das Sandbox-Szenario hat zwei engere Probleme freigelegt, beide rund um .env.

Eine Datei, die existiert, aber nicht lesbar ist. Der Sandbox-Launcher hält die .env des Profils zurück (unter Windows macht eine MXC-Deny-List sie unlesbar) und übergibt die erlaubten Keys stattdessen als Umgebungsvariablen. Die Datei existiert weiterhin, also öffnet load_hermes_dotenv sie, läuft in einen PermissionError, und Hermes stirbt beim Import — bevor irgendetwas läuft. Die neue Regel: wenn, und nur wenn, Managed Scope ein nicht-leeres terminal.backend pinnt, wird eine vorhandene, aber unlesbare .env des Profils als abwesend geladen. Aus ihr wird nichts angewendet, geerbte Keys bleiben, und eine Warnung nennt die Datei einmal pro Prozess (nicht einmal pro Turn). Ohne den Pin pflanzt sich der Fehler weiter fort, denn dort ist die Datei womöglich der einzige Ort, an dem das Backend steht — sie zu verschlucken wäre schlimmer.

Die .env des Projekts, die Keys stiehlt. Der alte Code entschied anhand eines erfolgreichen Ladens der Profil-.env, ob eine .env im Projektverzeichnis Lücken füllen darf. Jetzt wird Existenz geprüft: Solange die Profil-.env da ist (lesbar oder nicht), darf die Projekt-.env nur Lücken füllen, nie überschreiben. Die injizierten Keys des Launchers überleben eine Repo-.env.

Noch ein kleines Detail: Wenn die .env existiert, aber nicht gelesen werden kann, führen /reload und die Methode reload.env ihren Entfernungsdurchlauf nicht mehr aus — sonst würden die gerade injizierten Keys des Launchers wieder ausradiert.

Wie ein Administrator das pinnt

sudo mkdir -p /etc/hermes
sudo chmod 0755 /etc/hermes

sudo tee /etc/hermes/config.yaml >/dev/null <<'YAML'
terminal:
  backend: docker
  docker_image: registry.example.com/hermes-sandbox:2026-10
  docker_env:
    NODE_OPTIONS: --max-old-space-size=2048
YAML

sudo chmod 0644 /etc/hermes/config.yaml

In einem Container, in dem /etc unpraktisch ist, legt man das Verzeichnis stattdessen in der Service-Unit fest:

export HERMES_MANAGED_DIR=/opt/org/hermes-policy

Änderungen greifen beim nächsten Start von Hermes. Eine fehlerhafte Managed-Datei blockiert den Start nie — sie wird laut geloggt und ignoriert —, also prüfe mit hermes doctor, ob die gepinnten Keys tatsächlich übernommen wurden.

Woran ein Nutzer erkennt, dass er verwaltet wird

Drei Read-only-Befehle:

hermes config        # der Header nennt die Managed-Quelle und die gepinnten Keys
hermes doctor        # aufgelöstes Managed-Verzeichnis und Anzahl gepinnter Keys
hermes config get terminal.backend

Der Versuch, einen gepinnten Key zu ändern, wird mit Nennung der Quelle abgelehnt:

Cannot set 'terminal.backend': it is managed by your administrator
(/etc/hermes/config.yaml) and cannot be changed.

Wenn Befehle in einem Container weiterhin auf dem Host landen, prüfe in dieser Reihenfolge: was terminal.backend in hermes config meldet; ob das von hermes doctor aufgelöste Managed-Verzeichnis das erwartete ist; ob die config.yaml des Profils denselben Key setzt. Die vollständige Backend-Liste und wie Plugin-Backends installiert werden findest du im Terminal-Backend-Plugin-Guide; die Sicherheitsdetails des docker-Backends (unter anderem das Entfernen von setuid-Bits) stehen im docker-sandbox-Artikel.

Stand und Grenzen

Beide PRs (#145221 für das .env-Laden beim Start, #145222 für den Terminal-Scope pro Turn) wurden am 2026-10-29 in main gemerged und sind mit v0.22.6 (2026-11-02) in ein Release gekommen – ein normales Update reicht, eine main-Installation ist nicht mehr nötig.

Drei Grenzen sollte man kennen: Die verwaltete .env ist weltlesbar (0644), also keine hochsensiblen Secrets hineinschreiben; die Tools des Agenten selbst sind nicht hart daran gehindert, verwaltete Umgebungswerte zu lesen; und wenn ein Nutzer in das Managed-Verzeichnis schreiben kann (oder Hermes als root läuft), ist der Mechanismus nur beratend. Er stoppt Unfälle und beiläufige Änderungen, keine entschlossene Rechteausweitung — für echte Isolation gewinnen weiterhin getrennte Maschinen oder Container.