Advanced
Hook System
Intercept and modify data at 7 points in the agent loop. Block tool calls, transform messages, and react to session events.
Overview
The hook system lets you intercept data at key points in the agent loop. Hooks can inspect, modify, or block data as it flows through the system. This enables use cases like content filtering, logging, rate limiting, and custom security policies.
There are two types of hooks:
Modifying Hooks
Run sequentially in priority order. Can transform data or block it entirely. If one hook blocks, the pipeline stops and downstream hooks don't run.
Notification Hooks
Run in parallel. Fire-and-forget for observability. Cannot modify data or block the pipeline. Used for logging, analytics, and external integrations.
Hook Points
Omni provides 7 hook points — 5 modifying and 2 notification.
Modifying Hooks
Modifying hooks run sequentially in priority order (lowest number first). Each hook receives the output of the previous hook. A hook can return Continue (optionally with modified data) or Block to stop the pipeline.
Hook A (priority 10) → Continue(modified data)
↓
Hook B (priority 20) → Continue(data)
↓
Hook C (priority 30) → Block("reason")
↓
Pipeline stops. Hook D (priority 40) never runs.
When a BeforeToolCall hook blocks, the agent loop receives a HookBlocked error and skips the tool execution. The LLM is informed that the tool call was blocked.
Notification Hooks
Notification hooks run in parallel using tokio::join!. They receive a read-only copy of the hook context. Errors in notification hooks are logged but don't affect the pipeline.
Common use cases: sending analytics events, writing to external log services, triggering webhooks, or updating a dashboard when sessions start or end.
Hook Context
Every hook receives a context object containing relevant data for the hook point.
Hook Results
Modifying hooks return one of two results:
Continue(HookContext)
Let the pipeline proceed. Pass the context unchanged or with modifications. The next hook in the chain receives the returned context.
Block { reason: String }
Stop the pipeline. The reason string is logged and, for BeforeToolCall hooks, returned to the LLM as a HookBlocked error so it can adapt.
Registration
Hooks are registered with the HookRegistry which is shared across the agent loop via Arc<HookRegistry>.
let registry = HookRegistry::new();
// Register a modifying hook with priority 10
registry.register_modifying(
HookPoint::BeforeToolCall,
10, // priority
my_hook_handler,
);
// Register a notification hook
registry.register_notification(
HookPoint::SessionStart,
my_notification_handler,
);
Hook handlers implement the HookHandler trait, which has a single async method that receives a HookContext and returns a HookResult.
Examples
Block dangerous tool calls
A BeforeToolCall hook that prevents the agent from executing exec with destructive commands.
async fn handle(&self, ctx: HookContext) -> HookResult {
if let Some(tool_call) = &ctx.tool_call {
if tool_call.name == "exec" {
let cmd = tool_call.arguments
.get("command")
.and_then(|v| v.as_str())
.unwrap_or_default();
let blocked = ["rm -rf", "format", "del /f"];
if blocked.iter().any(|b| cmd.contains(b)) {
return HookResult::Block {
reason: "Destructive command blocked".into(),
};
}
}
}
HookResult::Continue(ctx)
}
Log all LLM requests
An LlmInput hook that logs the prompt being sent to the LLM for debugging.
async fn handle(&self, ctx: HookContext) -> HookResult {
if let Some(messages) = &ctx.messages {
tracing::debug!(
"LLM request: {} messages, session {{:?}}",
messages.len(),
ctx.session_id,
);
}
HookResult::Continue(ctx)
}
Filter profanity from messages
A MessageReceived hook that redacts profanity from user messages.
async fn handle(&self, mut ctx: HookContext) -> HookResult {
if let Some(ref mut text) = ctx.text {
*text = self.profanity_filter.censor(text);
}
HookResult::Continue(ctx)
}