Wenn Hermes auf einen Menschen wartet, kann dein Plugin das jetzt wissen

Es ist zwei Uhr morgens und dein Hermes-Lauf auf einem Server stößt auf einen Befehl, der ein sudo-Passwort will, oder es poppt ein Freigabe-Prompt für einen gefährlichen Befehl auf. Niemand sitzt davor. Der Lauf stoppt einfach, bis er in einen Timeout läuft und scheitert, und um neun am nächsten Morgen findest du eine kalte Zeile Ausgabe im Terminal. Was du wolltest, war trivial: Wenn es blockiert, sag mir Bescheid.
Das war früher umständlich zu bauen. Hermes stoppt an mehreren unabhängigen Stellen für einen Menschen — der sudo-Passwort-Prompt, eine Frage des clarify-Tools, eine Freigabe für einen gefährlichen Befehl — und jede liegt auf ihrem eigenen Code-Pfad. Um eine einzige Benachrichtigung „jemand wird gebraucht“ zu bekommen, musstest du einen Hook pro Prompt-Typ registrieren und dann Terminal-Befehlstrings per Muster abgleichen, um zu erraten, welche davon sudo waren.
PR #133171, am 5. Oktober 2026 zusammengeführt, schließt diese Lücke mit einem generischen Hook-Paar, das auslöst, wann immer der Agent stoppt und auf einen Menschen wartet.
Das Hook-Paar
on_human_input_request — fires right before the prompt is shown
on_human_input_resolved — fires once per request when the wait ends
Der Request-Hook sitzt in dem gemeinsamen Code, den jede Oberfläche ohnehin durchläuft, sodass CLI, Ink TUI / Desktop, ACP und Gateway-Plattformen (Telegram, Discord, …) alle abgedeckt sind, ohne pro Oberfläche etwas zu verdrahten. Was er abdeckt:
kind |
Welcher Prompt | Wo er erscheint |
|---|---|---|
"sudo" |
Maskierter sudo-Passwort-Prompt, bevor ein sudo-Befehl läuft |
Interaktives CLI, Ink TUI / Desktop (sudo.request-Karte), /dev/tty-Fallback |
"clarify" |
Eine Frage des clarify-Tools |
CLI, Ink TUI / Desktop, Gateway-Plattformen |
"approval" |
Freigabe für gefährlichen Befehl / Schreibvorgang, MCP-Elicitation-Consent, Plugin-Approval-Transporte | CLI, Ink TUI / Desktop, ACP, Gateway-Plattformen, Plugins mit ctx.register_approval_transport |
Zwei Fälle lösen bewusst nicht aus: approvals.mode=smart-Entscheidungen des Hilfsmodells, weil gar kein Mensch gefragt wird; und Gateway-Freigaben, die sich einem bereits wartenden identischen Prompt anschließen, weil die Person nur einen Prompt sieht und das Hook-Paar dafür einmal auslöst.
Was dein Callback erhält
def my_callback(kind: str, request_id: str, session_id: str, session_key: str,
platform: str, prompt: str, **kwargs):
...
| Parameter | Beschreibung |
|---|---|
kind |
"sudo", "clarify" oder "approval"; neue Arten können hinzukommen, also ignoriere die, die du nicht behandelst |
request_id |
Einmalig pro Prompt; das passende on_human_input_resolved trägt denselben Wert |
session_id |
Hermes-Session-ID, sofern bekannt, sonst "" |
session_key |
Routing-Key für Session/Chat, praktisch, um Benachrichtigungen pro Chat abzugrenzen |
platform |
Gateway-Plattform ("telegram", "discord", …) oder lokale Quelle ("cli", "tui", "desktop", …) |
prompt |
Wonach die Person gefragt wird: der Befehl bei sudo/approval, der Fragetext bei clarify |
Ein Detail zu prompt verdient einen eigenen Absatz. Es läuft immer durch den Secret-Redactor (redact_sensitive_text(..., force=True)), auch wenn security.redact_secrets in deiner Konfiguration aus ist. Wenn die Redaktion selbst fehlschlägt, ist prompt ein leerer String — der Rohtext wird nie gesendet. Das sudo-Passwort, das du eintippst, und die Antworten auf clarify-Fragen werden nie an einen der beiden Hooks übergeben; nur das Ergebnis-Label.
Rückgabewerte werden ignoriert, und ein Callback, der eine Exception wirft, wird auf Debug-Level geloggt, während der Prompt weiterläuft — ein kaputter Beobachter kann einen menschlichen Prompt nicht kaputt machen. Callbacks laufen auf dem Thread, der gleich blockiert, also halte sie schnell: Übergib Push-Service- oder Webhook-Aufrufe an einen Hintergrund-Thread.
Ergebniswerte
on_human_input_resolved löst genau einmal pro Anfrage aus, nachdem das Warten endet, mit einem zusätzlichen Feld:
kind |
outcome-Werte |
|---|---|
"sudo" |
"provided", "skipped" (leere Antwort), "timeout", "cancelled", "error" |
"clarify" |
das eigene Ergebnis der Oberfläche: "submitted", "timed_out", "cancelled", "undelivered", "terminated" oder "error" |
"approval" |
"once", "session", "always", "deny", "timeout", "cancelled", "notify_failed" oder "transport_<failure>" für einen fehlgeschlagenen Plugin-Transport |
Ein wirklich nützliches Beispiel: Benachrichtige mich nur, wenn niemand antwortet
Der häufige Bedarf ist nicht „benachrichtige mich bei jedem Prompt“, sondern „stups mich an, wenn sich niemand darum kümmert“. Das Request/Resolve-Paar macht das ganz natürlich:
import threading
def push_notification(text):
... # hier deinen Push-Service aufrufen
def register(ctx):
pending = {}
def on_request(kind, request_id, prompt, session_key, **kwargs):
pending[request_id] = threading.Timer(
30, push_notification, (f"Hermes needs you ({kind}): {prompt[:80]}",)
)
pending[request_id].start() # nur benachrichtigen, wenn niemand innerhalb von 30s antwortet
def on_resolved(request_id, **kwargs):
timer = pending.pop(request_id, None)
if timer:
timer.cancel()
ctx.register_hook("on_human_input_request", on_request)
ctx.register_hook("on_human_input_resolved", on_resolved)
Das Schöne daran ist, dass Prompts, die ein Mensch beantwortet, dich nie erreichen, und nur wirklich festgefahrene Läufe benachrichtigen. Weil request_id gepaart ist, bekommst du keine doppelten Benachrichtigungen oder verwaisten Timer. Zur Form des Hook-Systems als Ganzes — Observer-Hooks versus Interceptor-Hooks, Registrierung und Lifecycle — siehe die Hooks-Befehlsreferenz und die frühere Anleitung zu Provider-Plugin-Hooks.
Release-Status
Das Hook-Paar ist auf main und noch in keinem veröffentlichten Tag (die neueste Version ist v0.21.5, Tag v2026.9.24). Git-Installationen bekommen es, indem sie das neueste main ziehen; Docker-Images und gehostete Deployments erhalten es mit dem nächsten Tag. Wie du das Plugin selbst schreibst, steht in der Plugins-Befehlsreferenz.
Warum das über Benachrichtigungen hinaus wichtig ist
Einen Alarm „Hermes braucht einen Menschen“ zu bauen, erforderte früher Kenntnis der internen Code-Pfade für jeden Prompt-Typ. Jetzt ist es ein Hook-Paar, ein kind-Feld und eine Reihe von Ergebnis-Labels. Die übertragbarere Idee ist die Design-Entscheidung dahinter: „gestoppt und auf einen Menschen wartend“ als First-Class-Event behandeln, statt jeden Aufrufer seine eigene Sicht zusammenbauen zu lassen. Für unbeaufsichtigte Automatisierung ist das meist der Unterschied zwischen „einen festgefahrenen Lauf am nächsten Morgen entdecken“ und „davon erfahren, im Moment, in dem er hängt“.