Hook Lifecycle¶
OpenClaw Hook System¶
OpenClaw provides several hook points for plugins to intercept and modify behavior. The Prisma AIRS plugin uses 5 hooks for defense-in-depth.
Hook Types¶
Void Hooks (Fire-and-Forget)¶
Void hooks execute asynchronously. Their return values are ignored, and they cannot block or modify the event.
// OpenClaw extensionAPI.js (simplified)
async runVoidHook(hookName, event) {
const handlers = this.hooks.get(hookName);
for (const handler of handlers) {
handler(event).catch(err => console.error(err));
// Note: does not await, ignores return value
}
}
Used by: message_received
Modifying Hooks¶
Modifying hooks execute synchronously. They can return modifications that are merged into the event or context.
// OpenClaw extensionAPI.js (simplified)
async runModifyingHook(hookName, event) {
const handlers = this.hooks.get(hookName);
let result = {};
for (const handler of handlers) {
const mod = await handler(event);
if (mod) Object.assign(result, mod);
}
return result;
}
Used by: before_agent_start, before_tool_call, message_sending
Hook Events¶
before_agent_start (guard)¶
Fires when an agent initializes. The guard hook is registered at priority 100.
Event Shape:
interface AgentBootstrapEvent {
type: "agent";
action: "bootstrap";
context: {
workspaceDir?: string;
bootstrapFiles?: BootstrapFile[];
cfg?: Record<string, unknown>;
};
}
Plugin Hook: prisma-airs-guard
Modification: Adds to bootstrapFiles array
message_received¶
Fires when a message arrives at the gateway.
Event Shape:
interface MessageReceivedEvent {
from: string;
content: string;
timestamp?: number;
metadata?: {
to?: string;
provider?: string;
surface?: string;
threadId?: string;
messageId?: string;
senderId?: string;
senderName?: string;
};
}
Plugin Hook: prisma-airs-audit
Modification: None (void hook)
Cannot Block
message_received is fire-and-forget. The plugin caches scan results for downstream hooks to use.
before_agent_start¶
Fires before the agent begins processing a message.
Event Shape:
interface BeforeAgentStartEvent {
sessionKey?: string;
message?: {
content?: string;
text?: string;
};
messages?: Array<{
role: string;
content?: string;
}>;
}
Plugin Hook: prisma-airs-context
Modification:
interface HookResult {
prependContext?: string; // Prepended to agent context
systemPrompt?: string; // Alternative system prompt
}
before_tool_call¶
Fires before each tool invocation.
Event Shape:
interface BeforeToolCallEvent {
toolName: string;
toolId?: string;
params?: Record<string, unknown>;
}
Plugin Hook: prisma-airs-tools
Modification:
interface HookResult {
params?: Record<string, unknown>; // Modified params
block?: boolean; // Block the call
blockReason?: string; // Reason for blocking
}
message_sending¶
Fires before sending a response.
Event Shape:
interface MessageSendingEvent {
content?: string;
to?: string;
channel?: string;
metadata?: {
sessionKey?: string;
messageId?: string;
};
}
Plugin Hook: prisma-airs-outbound
Modification:
interface HookResult {
content?: string; // Modified content (or masked)
cancel?: boolean; // Cancel sending entirely
}
Hook Execution Timeline¶
T0: User sends message
│
T1: message_received fires (async)
│ └── prisma-airs-audit: scan, cache, log
│
T2: before_agent_start fires (sync)
│ └── prisma-airs-context: check cache, inject warnings
│
T3: Agent processes message
│
T4: Agent calls tool
│ └── before_tool_call fires
│ └── prisma-airs-tools: check cache, block if needed
│
T5: Agent generates response
│
T6: message_sending fires (sync)
│ └── prisma-airs-outbound: scan response, block/mask
│
T7: Response sent to user
Race Condition Handling¶
The message_received hook is async and may not complete before before_agent_start:
Timeline A (fast scan):
T0: message_received starts
T1: scan completes, cached
T2: before_agent_start fires, cache HIT
Timeline B (slow scan):
T0: message_received starts
T1: before_agent_start fires, cache MISS → fallback scan
T2: original scan completes (cached but unused)
The plugin handles this with fallback scanning in before_agent_start when cache misses occur.