Skip to main content

Warum Hooks existieren

Anweisungen in einem System-Prompt sind Vorschläge. Ein ausreichend eigensinniges oder verwirrtes LLM kann sie ignorieren. Für die meisten Agent-Verhaltensweisen ist das genau das, was Sie möchten — Anweisungen geben dem Modell Raum zur Anpassung. Aber einige Anforderungen sind keine Vorschläge. „Jeder sensible Tool-Aufruf muss protokolliert werden.” „Schreibvorgänge sind blockiert, wenn die Organisation im Nur-Lese-Modus ist.” „Zahlungen über ¥50k erfordern eine menschliche Bestätigung vor der Ausführung.” Das sind Invarianten — Fakten über das System, die unabhängig davon gelten müssen, was das Modell in einem bestimmten Durchlauf entscheidet. Ein Hook ist Code, der außerhalb der LLM-Schleife an einem klar definierten Punkt im Ausführungs-Lebenszyklus des Agenten läuft. Das LLM kann den Hook nicht sehen. Das LLM kann mit dem Hook nicht argumentieren. Das LLM kann den Hook nicht überreden, einen Schritt zu überspringen. Wenn ein PreToolUse Hook allow=False zurückgibt, findet der Tool-Aufruf nicht statt — egal wie hartnäckig die Reasoning-Spur war. Das ist die kritische architektonische Unterscheidung: Hooks sind, wie FIM One „der Agent soll…” in „der Agent kann nicht umgehen…” umwandelt.

Wo Hooks angebunden werden

Heute sind drei Hook-Punkte definiert. Jeder markiert eine Grenze, die der Agent während einer Schleifenwiederholung überschreitet: Mehrere Hooks am gleichen Punkt werden in Prioritätsreihenfolge ausgeführt. Die umgeschriebenen Args eines früheren PreToolUse-Hooks werden an spätere Hooks weitergeleitet, sodass sich Middleware zusammensetzt.

Wann ein Hook vs. eine Anweisung

Die Entscheidung, ob eine Anforderung mit einer Prompt-Anweisung oder einem Hook gelöst werden soll, ist dieselbe Berechnung wie “Runtime-Assertion vs. Code-Kommentar”: Faustregel: Wenn das falsche Verhalten ein Incident ist, verwenden Sie einen Hook. Wenn das falsche Verhalten eine kleine Unannehmlichkeit ist, ist eine Anweisung ausreichend.

Der Hook-Vertrag

Ein Hook ist eine Unterklasse von PreToolUseHook, PostToolUseHook oder SessionStartHook mit einer erforderlichen Methode:
Der übergebene HookContext enthält tool_name, tool_args, agent_id, user_id und ein flexibles metadata-Wörterbuch, das die Engine mit anfragespezifischen Fakten füllt (Organisations-ID, Konversations-ID, das requires_confirmation-Flag der Connector-Aktion, …). Das zurückgegebene HookResult steuert das Ergebnis:
  • allow: bool = True — ob der Tool-Aufruf fortgesetzt wird (wird für PostToolUse / SessionStart ignoriert)
  • error: str | None — benutzerfreundliche Begründung, die dem LLM als Beobachtung angezeigt wird, wenn blockiert
  • modified_args: dict | None — falls gesetzt, ersetzt die Tool-Argumente vor der Ausführung
  • modified_result: Any | None — falls gesetzt (PostToolUse), ersetzt die Beobachtung, bevor sie an das LLM zurückgegeben wird
  • side_effects: list[str] — Audit-Trail der Hook-Aktionen, zusammengeführt in die Agent-Trace

Fallstudie: FeishuGateHook

Der erste Hook, der auf diesem System implementiert wurde, ist FeishuGateHook — ein PreToolUse Hook, der jedes Tool mit dem Flag requires_confirmation=True in eine Genehmigungskarte mit menschlicher Kontrolle umwandelt, die in der Feishu-Gruppe der Organisation gepostet wird. Dieser Hook durchläuft den vollständigen Lebenszyklus: Das bringt dieses Design mit sich:
  • Der Tool-Aufruf wird wirklich unterbrochen. Der SSE-Stream des Agenten pausiert zwischen „Ich werde oa__purchase_pay aufrufen” und der Observation. Der Benutzer sieht den wartenden Agenten, was dem entspricht, was unter der Haube passiert.
  • Die Genehmigung übersteht einen Prozessneustart. Die ausstehende Zeile befindet sich in der Datenbank, nicht im Speicher. Wenn das Backend neu startet, während eine Karte ausstehend ist, nimmt die nächste Abfrage dort auf, wo sie aufgehört hat.
  • Die Entscheidung wird geprüft. ConfirmationRequest speichert payload, responded_at, responded_by_open_id und den endgültigen Status — ein überprüfbarer Datensatz darüber, wer was und wann genehmigt hat.
  • Kein LLM in der Entscheidungsschleife. Das Modell erzeugt den Tool-Aufruf. Menschen erzeugen das Urteil. Der Hook ist die deterministische Brücke.
FeishuGateHook hängt von einem konfigurierten Feishu Channel ab — der Hook sendet die Karte über die send_interactive_card()-Methode des Channels und wartet auf Callback-Events, die der Channel geparst hat. Die Trennung ist beabsichtigt: Der Hook besitzt die „Genehmigungszustandsmaschine”, der Channel besitzt die „IM-Plattformmechanik”. Der gleiche Hook könnte morgen auf Slack oder WeCom abzielen, ohne seine Logik zu ändern — nur die Channel-Implementierung.

Geplante Hooks (v0.9)

Vier Hook-Muster sind auf der v0.9-Roadmap geplant und verwenden alle denselben Lebenszyklus: Eine benutzerdefinierte Hook-Schicht ist ebenfalls geplant: Pro-Agent YAML-Konfiguration (hooks: [...]), die Shell-Befehle oder Python-Callables deklariert, um sie bei passenden Tool-Events auszuführen. Dies folgt dem gleichen Muster, auf das moderne Agent-Frameworks (Claude Code, OpenDevin) konvergiert haben — Hook-basierte Durchsetzung hält die „muss immer passieren”-Logik aus Prompts heraus.

Hooks vs. Channels

Die beiden Abstraktionen lösen orthogonale Probleme: Hooks nutzen Channels — ein Hook, der mit der Außenwelt kommunizieren muss (eine Karte senden, eine Benachrichtigung posten, an eine Gruppe eskalieren), ruft den Channel der Organisation auf. Ein Channel ohne einen Hook, der ihn nutzt, ist immer noch nützlich (z. B. können Agenten proaktiv Benachrichtigungen über ein Tool senden), aber das Approval-Gate-Muster erfordert speziell, dass beide Teile vorhanden sind. Anders ausgedrückt: Channels sind die „Wie kommuniziere ich mit Menschen”-Infrastruktur, Hooks sind die „Wann muss ich mit Menschen kommunizieren”-Richtlinie. Produktive Human-in-the-Loop-Workflows benötigen beides.

Current state (v0.8.4)

Snapshot of what shipped and what is still ahead:
  • HookRegistry, HookContext, HookResult primitives wired into both ReAct and DAG
  • PreToolUseHook / PostToolUseHook / SessionStartHook abstract bases
  • FeishuGateHook — complete, including ConfirmationRequest table, polling loop, timeout/expire, and callback-driven state flips
  • ✅ Feishu channel callback endpoint that decodes card.action.trigger and updates the pending row
  • ✅ Agent-level hook declarations: agent.model_config_json.hooks.class_hooks resolves to an instantiated HookRegistry on every ReAct/DAG session
  • Hooks fire on every execution surface: the main chat path (Portal, API, DAG), delegated sub-agents (CallAgentTool), and Workflow AGENT nodes. Eval Center runs intentionally bypass hooks (automated evaluation must not block on human approval). A delegated agent does not inherit its caller’s registry — it builds its own from its own model_config_json. Because the confirmation gate is auto-attached to every agent, rebuilding is a superset of inheriting: the delegate gets the gate plus whatever it declares for itself, and the gate resolves the delegate’s require_confirmation_for_all rather than its caller’s. Both surfaces fail closed: an agent whose hooks cannot be built is not run.
  • AuditLogHook, ReadOnlyGuard, ResultTruncateHook, ConnectorRateLimitHook (v0.9)
  • ❌ User-defined YAML hook declarations (v0.9)
The Hook System is a load-bearing foundation for v0.9 production hardening. Its first user (FeishuGateHook) is also a production feature in its own right, which is why the skeleton shipped early for the 2026-04-24 roadshow rather than waiting for the full hook catalog.