Der API-Key, den du einmal ausgegeben hast, darf nicht im Chat-Log landen

Es ist zehn Uhr abends, und du debuggst einen 401. Du machst im Terminal ein cat .env, um zu sehen, ob der Key richtig gesetzt ist, und Hermes liest diese Ausgabe in die Session ein. Zwanzig Minuten später postet der geplante Job, den du vorher eingerichtet hast, „heutiger Config-Check“ in die Telegram-Gruppe deines Teams — mit deinem vollständigen API-Key mitten in der Nachricht. Das Einzige, was du getan hast, war ein Blick in deine eigene Konfigurationsdatei.
Das ist die eigentliche Gestalt von Credential-Leaks: Niemand bricht ein und stiehlt etwas. Derselbe Text wandert nur von der Tool-Ausgabe in die Session-History und von dort in eine ausgehende Nachricht — und an einer dieser Stationen fühlt sich niemand fürs Maskieren zuständig. Zwei am 18. September 2026 gemergte Fixes stopfen je eine dieser Stationen: PR #114839 repariert die Maskierungsregeln für Tool-Ausgaben, PR #114789 repariert die Redaktionspolitik auf den Cron-Auslieferungswegen. Hier steht, was beide tun — und, genauso wichtig, wovor sie weiterhin nicht schützen.
Station eins: Die Maskierungsregeln verpassten zwei echte Credential-Formen
Hermes hat einen Redactor (agent/redact.py), durch den Terminal-Ausgaben, Dateilesevorgänge und alles, was daran hängt — Session-Speicherung, session_search —, hindurchläuft. Er erkennt Anbieter über eine Präfixtabelle: ghp_, xoxb-, AKIA, hf_, pplx- und so weiter. Er ist standardmäßig aktiv und wird über security.redact_secrets gesteuert (beim Abschalten wird eine Warnung beim Start geloggt).
Die Lücke waren zwei Formen, die es nie in diese Tabelle geschafft hatten:
sk--Keys mit Punkten aus dem Alibaba-Stack. Keys wiesk-sp-…undsk-ws-…enthalten Punkte im Rumpf, und die alte Zeichenklasse enthielt kein., sodass der Treffer am ersten Punkt endete: Der Kopf wurde maskiert, der Rest blieb im Klartext. Ein Key, der redigiert aussieht, während die Hälfte von ihm überlebt, ist schlimmer als einer, den man klar als unangetastet erkennt.- Zhipus präfixlose
id.secret-Keys. Ohne ein Präfix im Stil vonsk-/ghp_konnte die Präfixtabelle sie nie treffen, und das Credential wanderte vollständig im Klartext mit.
Der Fix tut zwei Dinge. Der sk--Eintrag akzeptiert jetzt einen punktgetrennten Rumpf, sodass sk-sp-… und sk-ws-… vollständig matchen und ein Satzpunkt hinter dem Key nie mitgefressen wird. Für Zhipu wurde ein bewusst anbieterspezifisches Regex ergänzt: eine 32-stellige Hex-ID in Kleinschreibung, ein Punkt, dann ein Credential-Segment aus mindestens 16 alphanumerischen Zeichen. Der Grund, nicht zu einer generischen Regel für „hoch-entropische punktierte Tokens“ zu greifen, sind False Positives: Dateinamen aus Content-Hashes wie <sha>.bundle oder <md5>.sqlite3 dürfen nicht matchen. Genau dafür enthalten die Tests Negativkontrollen — ein nackter 40-stelliger Hex-SHA, ein kurzes sk--Fragment und eine auf 31 Zeichen abgeschnittene ID müssen byteidentisch wieder herauskommen.
Gemessen an einer echten Probe:
| Eingabe | Vorher | Nachher |
|---|---|---|
api_key: sk-sp-...7890.abcdefgh1234567890_XYZ-0987654321 |
der Rest lief im Klartext mit | sk-sp-...4321 |
api_key: 50aaedFAKE1234567890abcdef1234567890abc.ZpSh99 |
unverändert | 50aaed...Sh99 |
dieselbe Datei über file_read |
«redacted:sk-…».abcdefg… — der Rest überlebte |
«redacted:sk-…» / «redacted-secret» |
Die dritte Zeile verdient einen eigenen Absatz: Der Pfad für Dateiinhalte maskiert nicht, er ersetzt durch einen Sentinel. Dahinter steht ein echter Vorfall (Issue #35519). Ein Agent las einen Key, der zu einer Form maskiert worden war, die wie ein echter, aber abgeschnittener Credential aussah, schrieb ihn zurück in config.yaml und zerstörte das gespeicherte Credential damit dauerhaft zu einem toten 13-Zeichen-Wert — ab dann nur noch 401. Der Sentinel («redacted-secret») ist syntaktisch als nutzbares Token unmöglich und kann daher nie für eines gehalten oder zurückgeschrieben werden. Auch die Performance hielt: pathologische Eingaben — ein Rumpf mit 20.000 Zeichen, 10.000 Punkte, 2.000 Hex-Läufe — werden jeweils in unter 3 Millisekunden redigiert.
Station zwei: Jeder Cron-Auslieferungsweg redigiert jetzt — zwangsweise
Die Tool-Ausgabe war der eine Weg. Der leichter zu vergessende ist, dass die Ausgabe eines Cron-Jobs das System konstruktionsbedingt verlässt. In der früheren Implementierung wurden stdout und stderr eines Skript-Jobs dort bereinigt, wo sie abgefangen wurden; der Antworttext eines LLM-Jobs erreichte _deliver_result dagegen ungeprüft — und jeder Weg darunter verschickte den rohen String.
PR #114789 setzt die Redaktion an den einen zentralen Auslieferungspunkt und ergänzt einen Fail-Closed-Backstop:
cron/scheduler_delivery.pyerhält_redact_cron_payload(), dasredact_sensitive_text(text, force=True)aufruft.- Was
force=Truebedeutet: Hier liegt eine Egress-Grenze.security.redact_secretsregelt die Logs auf deiner eigenen Maschine; hier geht es darum, was diese Maschine verlässt. Die Nutzereinstellung kann die Redaktion an dieser Stelle also nicht abschalten. - Fail closed: Wenn der Redactor selbst eine Exception wirft, wird die gesamte Payload durch
[REDACTED - redaction failed]ersetzt, statt sie durchzureichen. Das entspricht der Haltung, die bereits dort gilt, wo Shell-stdout abgefangen wird. - Es deckt vier Wege ab: die Plattform-Nachricht (den Chokepoint
cleaned_delivery_content), den Text des Session-Mirrors (der aus dem rohencontentabgeleitet wird und den Chokepoint deshalb nicht geerbt hat), den Job-Namen, der bis in Mirror-Präfixe und Thread-Titel reicht, und den an ein anderes Profile übergebenen Bot-Chat-Turn — dort wirdcontentneu gebunden, sodass auch der dauerhafte Deferred-Record und dessen Replay die bereinigte Kopie tragen. - Der Weg für Fehlerbenachrichtigungen brauchte keine eigene Änderung: Er verlässt das System über denselben Chokepoint
_deliver_resultund ist damit konstruktionsbedingt abgedeckt.
Eine bewusste Ausnahme sollte man kennen: redact_url_credentials ist für Cron nicht aktiviert. Diese Jobs liefern legitim Magic Links und vor-signierte URLs in den eigenen Chat, und würde man URLs mit Credentials pauschal entfernen, wäre das Feature kaputt. Das ist dieselbe Abwägung, die agent/redact.py bereits für Chat-Egress-Grenzen dokumentiert.
Was das weiterhin nicht abdeckt
Beide Fixes sind echt, aber sie betreffen die Ausgaben von Hermes selbst. Drei Dinge bleiben deine Verantwortung:
- Klebe keine Credentials in die Konversation. Text, den du selbst einfügst, landet ebenso in der Session-History und in der Datenbank. Der Redactor gibt bei bekannten Formen sein Bestes, aber „bekannte Formen“ sind immer eine unvollständige Liste, und ein Key von Drittanbietern hat womöglich gar keine erkennbare Form. Um einen Konfigurationswert zu prüfen, nutze
hermes config get <key>und bestätige Vorhandensein, Länge oder die letzten vier Zeichen — nicht den Wert selbst. - Gib keine sensiblen Dateien aus Cron-Skripten aus. Der Auslieferungsweg redigiert, aber die Logs des Skripts selbst, deine CI-Ausgabe und deine Shell-History können das Original weiterhin enthalten. Gib das Ergebnis aus („KEY present“ / „KEY missing“), nie den Wert.
- Schalte die Redaktion nicht ab, um Frust loszuwerden.
security.redact_secrets: falsebetrifft nur den lokalen Log-Pfad und protokolliert eine Warnung beim Start; den Cron-Weg kann es nicht erreichen, der ist zwangsweise aktiv. Wenn du den Redactor selbst debuggst, schalte ihn danach wieder ein.
Für den größeren Zusammenhang beim Speichern und Rotieren von Credentials siehe den Keychain-Verschlüsselungsleitfaden. Wenn dein Benachrichtigungsweg eine Chat-Plattform ist, behandelt der Leitfaden zu unbeaufsichtigten Freigaben die andere Hälfte dieser Kontrolle, und für die Jobs selbst beginnst du am besten mit dem Cron-Troubleshooting-Leitfaden und der hermes secrets-Referenz.
Release-Status
Beide PRs wurden am 18. September 2026 in main gemergt, und der neueste Tag ist v0.21.3 (Tag-Name v2026.9.14, veröffentlicht am 14. September), keiner der beiden ist also schon in einem getaggten Release. Eine Git-Installation nimmt sie beim nächsten Update mit:
hermes --version
hermes update
Docker-Image- und Cloud-Nutzer bekommen sie mit dem darauffolgenden Tag.
Das Muster, das man behalten sollte
Zusammen betrachtet ist die Regel einfach: Redaktion ist eine Liste, die aktuell bleiben muss, sie gehört an den einen Egress-Punkt, und sie braucht einen Fail-Closed-Backstop. Einen Namen in die Präfixtabelle aufzunehmen reicht nicht, wenn der Rumpf Punkte enthalten kann; ein paar Wege zu bereinigen reicht nicht, wenn der Session-Mirror und der Job-Name denselben Text hinaustragen; und ein Redactor, der im Fehlerfall auf den Originaltext zurückfällt, schützt überhaupt nichts. Genau diese drei Nahtstellen wurden repariert.