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:
- die eingebauten Defaults;
- die
.envdes Profils; - das Launch-Overlay (Werte, die auf der Kommandozeile übergeben werden);
- die
config.yamldes 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: dockerdes Administrators ist inhermes configsichtbar; - die Befehlsausführung nutzt trotzdem das
localdes 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:
nullpinnt nichts.docker_env: nullin 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_imagegilt 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.