Ein Plugin kann Hermes jetzt bitten, seinen eigenen MCP-Server abzusichern

Ein Community-Plugin aus dem Katalog installieren — und schon registriert es still und leise einen MCP-Server, der in deinem Namen Bestellungen aufgeben, Geld bewegen oder Nachrichten verschicken kann. Hermes fragt dich kein einziges Mal. Das Einzige, was zwischen einem per Prompt-Injection manipulierten Modell und der Aktion steht, ist ein Satz im Skill-Text des Plugins, der das Modell bittet, vorher bei dir nachzufragen — und ob das Modell sich daran hält, ist eine Frage der Tagesform, nicht des Codes.
Das ist nicht die Schuld des Plugins. Es ist ein Loch im Format: Agent Plugins v1 definiert kein trust-Feld, also lief jeder MCP-Server aus einem portablen Paket mit dem Default trust: full. PR #134524, gemerged am 7. Oktober 2026, schließt diese Lücke: Ein Paket kann Hermes bitten, seinen eigenen Server abzusichern, und kann nichts Weitergehendes verlangen.
Woher die Lücke kam
Hermes unterstützt zwei Plugin-Formen. Native Plugins sind eine plugin.yaml plus eine register(ctx)-Funktion, fest im Core verdrahtet. Portable Pakete zielen auf das Format Agent Plugins v1.0.0 und sehen so aus:
my-portable-plugin/
├── plugin.json
├── skills/
│ └── summarize/
│ ├── SKILL.md
│ └── references/
└── mcp.json
plugin.json ist das Manifest, mcp.json deklariert die MCP-Server, mit denen sich das Paket verbindet, und skills/ enthält die ausgelieferten Skills. Ein frisch installiertes Paket bleibt deaktiviert, bis du hermes plugins enable <name> ausführst — gute Hygiene, aber das beantwortet nur die Frage, ob überhaupt geladen wird, nicht, wer die Bremse zieht, während es läuft.
Das eigentliche Problem war der Reader. Das portable mcp.json von Hermes akzeptiert nur type, url und headers (siehe hermes_cli/agent_plugins.py), während die Trust-Stufe in der nativen Konfiguration unter mcp_servers.<name>.trust in config.yaml lebt. Ohne ein solches Feld in der Spec hat der Reader nie eines übersetzt, und jeder Paket-Server startete mit dem Default full.
Auf die Roadmap kam das durch einen konkreten Fall. Ein Katalog-Sweep förderte ein Trading-Plugin zutage, dessen gehosteter Server jeden Trade auf dem Hyperliquid-Konto des Nutzers anhält, bis resolve_approval aufgerufen wird. Wie der PR es formuliert: Das Einzige, was zwischen einem per Prompt-Injection manipulierten Modell und einem genehmigten Trade stand, war der Skill-Text. Hermes selbst hat nie nachgefragt.
Wie ein Paket ein Gate anfordert
plugin.json trägt bereits einen Hermes-spezifischen Extension-Block — denselben, der app, requires und liveness enthält. Er akzeptiert jetzt trust:
{
"extensions": {
"com.nousresearch.hermes": {
"servers": {
"trade": { "trust": "untrusted" }
}
}
}
}
Dieses untrusted wird in den übersetzten mcp.json-Server-Eintrag geschrieben, und von dort greift das bestehende Gate in tools/mcp_tool_handlers.py unverändert:
- Jeder Tool-Aufruf ohne eine
readOnlyHint: true-Annotation fragt zuerst bei dir nach, über die standardmäßige Approval-Oberfläche; - wo niemand antworten kann — cron-Jobs,
hermes chat -q-One-Shots, eine unbeaufsichtigte Gateway-Session — schlägt der Aufruf fail-closed fehl, statt durchzurutschen; - ein Eintrag, der trust und sonst nichts deklariert, erzeugt kein Availability-Gate; er legt nur ein Lock auf den Server.
Nur verengen, konstruktionsbedingt
Die Design-Regel ist wichtiger als das Feature. Ein Paket darf genau zwei Werte deklarieren:
| Was das Paket schreibt | Was passiert |
|---|---|
untrusted |
Wird angewendet — jeder schreibfähige Aufruf auf diesem Server braucht Approval |
full |
Wird akzeptiert, ist aber ohnehin schon der Default, gewährt also nichts |
| Alles andere | Das Paket wird komplett abgelehnt, dieselbe fail-closed-Haltung wie bei einem unbekannten Wert in der nativen Konfiguration |
Eine weitere Vorrangregel sichert das ab: Ein config.yaml-Server mit demselben Namen ersetzt den Eintrag des Pakets vollständig, trust inklusive. Ein Paket kann den Nutzer nie überstimmen. Setzt du einen Server selbst auf full, ändert kein noch so großer untrusted-Eintrag im Manifest daran etwas.
Die Formulierung des PRs ist es wert, festgehalten zu werden: Das existiert, damit ein Paket verengen, niemals erweitern kann. Das Extension-Feld ist kein Weg für Plugins, Autorität zu gewinnen, sondern einer, Autorität zurückzugeben.
Prüfen, bevor du aktivierst
hermes plugins validate listet jetzt die gated Server auf, damit Katalog-Reviewer — und du — die Behauptung sehen können:
server trust: trade — untrusted (Hermes asks before every write-capable tool call)
Wenn ein unbekanntes portables Paket auftaucht, ist diese Reihenfolge am günstigsten für dich:
hermes plugins validate /path/to/package # was deklariert es, inklusive gegateter Server
hermes plugins install owner/repo # installiert deaktiviert
hermes plugins capabilities <name> # deklarierte vs. gewährte Fähigkeiten
hermes plugins enable <name> # erst einschalten, wenn du einverstanden bist
Wenn du Plugins veröffentlichst
Drei Dinge, die man mitnehmen sollte:
- Gate deine Schreib-Server. Jeder MCP-Server, der Geld ausgibt, handelt, Nachrichten verschickt oder Accounts verändert, sollte
trust: "untrusted"inplugin.jsondeklarieren. Verlass dich nicht auf Skill-Text, der das Modell zum Nachfragen auffordert — das ist ein Hinweis, kein Gate. - Niemals Credentials in
mcp.json. Die dort deklariertenenv-Werte sind sichtbare Paketdaten, kein Secret Store. - „Installiert deaktiviert“ ist kein Gate. Sobald ein Nutzer das Paket aktiviert, ist der Extension-Block die einzige Sicherheitsaussage, die du kontrollierst.
Für das weiter gefasste Trust-Modell bei remote MCP-Servern — warum readOnlyHint ein vom Server gelieferter Hinweis ist und warum nicht annotierte Tools outcome_uncertain zurückgeben, wenn eine Transport-Session mitten im Aufruf abläuft — siehe das Security-Modell des Remote-MCP-Katalogs. Wie weit Hermes ein Plugin vom Host isoliert, steht in Plugin-Isolation und die Host-Grenze, und wie sich Approvals verhalten, wenn niemand da ist, vertieft der Leitfaden zu unbeaufsichtigten Approvals. Kommandoreferenz: hermes plugins.
Der Teil, den man sich merken sollte
readOnlyHint ist ein Hinweis, den der Server über sich selbst gibt — ein lügender Server kann höchstens einen Prompt für ein Tool überspringen, das er als read-only ausgibt, aber nie zusätzlichen Zugriff gewinnen. trust: untrusted macht die Abhängigkeit von jeglicher Selbstbeschreibung zunichte: Alles, was nicht als read-only markiert ist, fragt zuerst nach.
Dieser Unterschied ist der ganze Punkt der Änderung. Eine Sicherheitsgrenze sollte nicht von der Partei gezogen werden, die geprüft wird, und bis zu diesem PR konnte ein portables Paket überhaupt keine ziehen. Jetzt kann es das — zum Preis einer zusätzlichen Zeile im Manifest.