Hermes soll deine SSH-Config bearbeiten? Jetzt fragt es vorher nach


Du bittest Hermes, einen neuen Server in deine SSH-Konfiguration aufzunehmen – einen Host-Alias, einen ProxyJump-Bastion, damit ssh work ab sofort mit einer einzigen Zeile verbindet. write_file knallt die Tür zu: „Write denied: ~/.ssh/config is a protected system/credential file.“ Also wechselst du zum terminal-Tool und schreibst dieselben Zeilen per printf in dieselbe Datei – und es erscheint nur ein Bestätigungsdialog, du klickst auf Freigeben, fertig. Dieselbe Datei, zwei Tools, zwei widersprüchliche Regelwerke – welches hat recht? PR #84663, gemergt am 12. August, klärt das: Die SSH-Client-Konfiguration ist keine Zone mehr, an die man nicht einmal denken darf – sie wurde zu einem Approval Gate, das zuerst fragt.

Die Ursache: eine Datei, zwei widersprüchliche Regeln

Der Widerspruch kam von zwei unabhängigen Sicherheitsebenen:

  • write_file / patch prüfen die hart gesperrte Liste in agent/file_safety.py. ~/.ssh/config traf bisher sowohl den exakten Pfad-Deny als auch das ~/.ssh/-Präfix-Deny und wurde daher ohne Verhandlungsspielraum abgelehnt.
  • terminal läuft durch die Approval-Logik für gefährliche Befehle in tools/approval.py. Schreibzugriffe auf ~/.ssh wurden nur als freigabepflichtig markiert – Freigabe erteilt, und der Schreibvorgang geht durch.

Dieselbe ~/.ssh/config war also über die Datei-Tools eine Sackgasse und über das Terminal nur einen Klick von der Bestätigung entfernt. Dieses Hin und Her verwirrte die Nutzer – und der Agent selbst meldete erst „Schreiben fehlgeschlagen“ und dann über den anderen Weg „erfolgreich“, und hinterließ eine Spur widersprüchlicher Logs.

Der Fix: ~/.ssh/config von hart gesperrt zu freigabepflichtig

Die Argumentation von PR #84663: Die SSH-Client-Konfiguration ist kein Credential-Material. Sie enthält keine Private-Key-Bytes, und sie zu bearbeiten (Host-Aliase, ProxyJump, VS-Code-Remote-SSH-Ziele) ist eine alltägliche, vom Nutzer angestoßene Aufgabe – eine komplette Ablehnung ist falsch. Aber sie kann ProxyCommand- und Match exec-Direktiven enthalten, die Befehle ausführen, daher ist ein stilles Freischreiben genauso falsch. Freigabe ist die richtige Politik – passend zu dem, was das terminal-Tool für ~/.ssh-Schreibvorgänge bereits tat.

Verifiziert gegen origin/main:

  • agent/file_safety.py: ~/.ssh/config aus dem flachen Credential-Deny entfernt; neue Funktionen build_write_approval_paths() und is_write_approval_required() sorgen per Short-Circuit dafür, dass freigabepflichtige Pfade das ~/.ssh/-Präfix-Deny in _classify_write_denial überspringen.
  • Private Schlüssel und Auth-Dateien bleiben hart gesperrt: id_rsa, id_ed25519, authorized_keys und alles andere unter ~/.ssh/ – keine Ausnahmen.
  • tools/file_tools.py: write_file und patch leiten SSH-Config-Schreibvorgänge jetzt direkt nach dem Gate für geschützte Instruktionen durch das gemeinsame _run_approval_gate.
  • Nicht-interaktive Aufrufer sind fail-closed: Die ACP-Dateibrücke (agent/copilot_acp_client.py) lehnt freigabepflichtige Pfade rundweg ab; der TTS-Ausgabepfad-Wähler (tools/tts_tool.py) verweigert sie ebenfalls.

Das Datei-Sicherheits-Stufenmodell von Hermes

Diese Änderung bringt auch das vollständige Schreib-Sicherheitsmodell ans Licht, das drei Stufen hat, die man sich merken sollte:

Stufe 1: hart gesperrt – gar nicht erst daran denken. Credentials und kritische Systemdateien sind für kein Tool in keinem Modus beschreibbar, yolo eingeschlossen: ~/.ssh/authorized_keys, id_rsa, id_ed25519, .env, .anthropic_oauth.json, .netrc, .pgpass, .npmrc, .pypirc, .git-credentials, /etc/sudoers, /etc/passwd, /etc/shadow, plus Verzeichnis-Präfixe wie ~/.ssh/, ~/.aws/, ~/.gnupg/, ~/.kube/, ~/.docker/, ~/.config/gh/. Das ist das Fundament, das sich nie bewegt.

Stufe 2: freigabepflichtig – erst fragen. Aktuell ein Mitglied: ~/.ssh/config. Beschreibbar, aber nur über eine menschliche Freigabeabfrage, weil es befehlsausführende Direktiven einschmuggeln kann. Das Gate bietet drei Persistenz-Umfänge: once (dieses eine Mal), session (für diese Sitzung merken), always (für immer merken).

Stufe 3: freies Schreiben – alles andere. Normale Arbeitsdateien und Projektcode darf der Agent frei schreiben.

Wie sich das Approval Gate tatsächlich verhält

Wenn man das gemeinsame Gate liest (_run_approval_gate in tools/approval.py), ergibt sich diese Entscheidungsreihenfolge:

  1. --yolo geht zuerst durch: Der Yolo-Modus (prozessweites HERMES_YOLO_MODE oder sitzungsweites) passiert direkt – aber Stufe-1-Hart-Sperren werden vor dem Gate geprüft, also kann yolo sie weiterhin nicht anfassen;
  2. Session-Cache-Short-Circuit: Ein zuvor freigegebener Schreibvorgang (Umfang session/always) geht stillschweigend durch;
  3. Interaktiv-/Gateway-/Cron-Zweig: Interaktive Sitzungen bekommen eine Abfrage; Cron-Sitzungen folgen approvals.cron_mode (Standard: ablehnen);
  4. Fail-closed ohne menschlichen Kanal: Weder ein interaktives Terminal noch eine Gateway-Sitzung (z. B. Hintergrundskripte, ACP-Aufrufe) → GESPERRT, rundweg. Die Ablehnung sagt dem Agent sogar, er solle es nicht über terminal oder execute_code erneut versuchen, es sei denn, der Nutzer stimmt ausdrücklich zu.

Fazit

Die Umstellung von ~/.ssh/config von hart gesperrt auf freigabepflichtig bedeutet im Kern, die Entscheidung von „das Tool entscheidet“ zurück zu „du entscheidest“ zu verlagern: Alltägliche Bearbeitungen werden nicht mehr pauschal blockiert, während befehlstragende Schreibvorgänge immer durch ein menschliches Gate laufen. Mehr zur Approval-Mechanik von Hermes findest du in unserem Drei-Gate-Smart-Approvals-Leitfaden und darin, wie Approval-Fatigue gelöst wird; die --yolo-Modi und roten Linien behandelt dieser Beitrag; Konfigurationsbefehle stehen auf der hermes-config-Referenzseite.