Documentation Index
Fetch the complete documentation index at: https://mintlify.com/openclaw/openclaw/llms.txt
Use this file to discover all available pages before exploring further.
Hooks let you intercept and modify OpenClaw’s behavior at key lifecycle points. Hooks are registered via the Plugin API and called synchronously or asynchronously depending on the event type.
Registering Hooks
Use api.registerHook to register a hook handler:
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
const plugin = {
id: "my-plugin",
register(api: OpenClawPluginApi) {
api.registerHook("llm_input", async (event, ctx) => {
api.logger.info(`LLM call: ${event.provider}/${event.model}`);
});
}
};
export default plugin;
You can register multiple events at once:
api.registerHook(["message_received", "message_sent"], async (event, ctx) => {
api.logger.info(`Message event: ${JSON.stringify(event)}`);
});
Available Hooks
Agent Lifecycle
before_model_resolve
Fired before resolving the model/provider for an agent run. Use this to override model selection based on the prompt.
api.registerHook("before_model_resolve", async (event, ctx) => {
// event.prompt: string
// ctx: { agentId?, sessionKey?, workspaceDir?, messageProvider? }
if (event.prompt.includes("image")) {
return {
modelOverride: "gpt-4-vision-preview",
providerOverride: "openai"
};
}
});
Event: { prompt: string }
Return: { modelOverride?: string, providerOverride?: string } or void
before_prompt_build
Fired before building the prompt for an agent run. Session messages are available at this point.
api.registerHook("before_prompt_build", async (event, ctx) => {
// event.prompt: string
// event.messages: unknown[]
// ctx: { agentId?, sessionKey?, workspaceDir?, messageProvider? }
return {
systemPrompt: "Custom system prompt",
prependContext: "Additional context"
};
});
Event: { prompt: string, messages: unknown[] }
Return: { systemPrompt?: string, prependContext?: string } or void
before_agent_start (legacy)
Fired before an agent run starts. Combines both before_model_resolve and before_prompt_build phases.
api.registerHook("before_agent_start", async (event, ctx) => {
return {
modelOverride: "gpt-4",
systemPrompt: "Custom prompt"
};
});
Event: { prompt: string, messages?: unknown[] }
Return: { modelOverride?: string, providerOverride?: string, systemPrompt?: string, prependContext?: string } or void
Fired before sending a prompt to the LLM. Use for logging, telemetry, or analytics.
api.registerHook("llm_input", async (event, ctx) => {
api.logger.info(
`LLM call: ${event.provider}/${event.model}, ` +
`prompt length: ${event.prompt.length}, ` +
`history: ${event.historyMessages.length} messages`
);
});
Event:
{
runId: string;
sessionId: string;
provider: string;
model: string;
systemPrompt?: string;
prompt: string;
historyMessages: unknown[];
imagesCount: number;
}
llm_output
Fired after receiving a response from the LLM.
api.registerHook("llm_output", async (event, ctx) => {
api.logger.info(
`LLM response: ${event.assistantTexts.length} messages, ` +
`tokens: ${event.usage?.total ?? 0}`
);
});
Event:
{
runId: string;
sessionId: string;
provider: string;
model: string;
assistantTexts: string[];
lastAssistant?: unknown;
usage?: {
input?: number;
output?: number;
cacheRead?: number;
cacheWrite?: number;
total?: number;
};
}
agent_end
Fired after an agent run completes (success or failure).
api.registerHook("agent_end", async (event, ctx) => {
api.logger.info(
`Agent run ${event.success ? "succeeded" : "failed"}, ` +
`duration: ${event.durationMs}ms`
);
});
Event:
{
messages: unknown[];
success: boolean;
error?: string;
durationMs?: number;
}
Session Management
session_start
Fired when a new session starts.
api.registerHook("session_start", async (event, ctx) => {
api.logger.info(`Session started: ${event.sessionId}`);
});
Event: { sessionId: string, resumedFrom?: string }
session_end
Fired when a session ends.
api.registerHook("session_end", async (event, ctx) => {
api.logger.info(
`Session ended: ${event.sessionId}, ` +
`${event.messageCount} messages, ` +
`duration: ${event.durationMs}ms`
);
});
Event: { sessionId: string, messageCount: number, durationMs?: number }
before_compaction
Fired before session compaction (history summarization). Useful for archiving session history.
api.registerHook("before_compaction", async (event, ctx) => {
api.logger.info(
`Compacting ${event.compactingCount} messages, ` +
`total: ${event.messageCount}`
);
// Read full session history from disk if needed
if (event.sessionFile) {
const fs = await import("node:fs/promises");
const lines = await fs.readFile(event.sessionFile, "utf-8");
// Process lines...
}
});
Event:
{
messageCount: number;
compactingCount?: number;
tokenCount?: number;
messages?: unknown[];
sessionFile?: string;
}
after_compaction
Fired after session compaction completes.
api.registerHook("after_compaction", async (event, ctx) => {
api.logger.info(
`Compaction complete: ${event.compactedCount} messages kept`
);
});
Event: { messageCount: number, tokenCount?: number, compactedCount: number, sessionFile?: string }
before_reset
Fired when a session is reset (/new, /reset commands).
api.registerHook("before_reset", async (event, ctx) => {
api.logger.info(`Session reset: ${event.reason ?? "unknown"}`);
});
Event: { sessionFile?: string, messages?: unknown[], reason?: string }
Message Flow
message_received
Fired when a message is received from a channel.
api.registerHook("message_received", async (event, ctx) => {
api.logger.info(
`Message from ${event.from}: ${event.content.slice(0, 50)}`
);
});
Event: { from: string, content: string, timestamp?: number, metadata?: Record<string, unknown> }
Context: { channelId: string, accountId?: string, conversationId?: string }
message_sending
Fired before sending a message to a channel. Can modify or cancel the message.
api.registerHook("message_sending", async (event, ctx) => {
// Filter profanity
const filtered = event.content.replace(/badword/gi, "***");
return { content: filtered };
});
Event: { to: string, content: string, metadata?: Record<string, unknown> }
Return: { content?: string, cancel?: boolean } or void
Context: { channelId: string, accountId?: string, conversationId?: string }
message_sent
Fired after a message is sent to a channel.
api.registerHook("message_sent", async (event, ctx) => {
if (event.success) {
api.logger.info(`Message sent to ${event.to}`);
} else {
api.logger.error(`Failed to send: ${event.error}`);
}
});
Event: { to: string, content: string, success: boolean, error?: string }
Context: { channelId: string, accountId?: string, conversationId?: string }
Fired before a tool is called. Can modify parameters or block the call.
api.registerHook("before_tool_call", async (event, ctx) => {
if (event.toolName === "delete_file" && !event.params.confirmed) {
return {
block: true,
blockReason: "Destructive operation requires confirmation"
};
}
});
Event: { toolName: string, params: Record<string, unknown> }
Return: { params?: Record<string, unknown>, block?: boolean, blockReason?: string } or void
Context: { agentId?: string, sessionKey?: string, toolName: string }
Fired after a tool completes.
api.registerHook("after_tool_call", async (event, ctx) => {
api.logger.info(
`Tool ${event.toolName} ${event.error ? "failed" : "succeeded"}, ` +
`duration: ${event.durationMs}ms`
);
});
Event: { toolName: string, params: Record<string, unknown>, result?: unknown, error?: string, durationMs?: number }
Context: { agentId?: string, sessionKey?: string, toolName: string }
Fired before writing a tool result to the session transcript. Can modify or drop the message.
api.registerHook("tool_result_persist", (event, ctx) => {
// Strip large responses to save space
if (event.message.content?.length > 10000) {
return {
message: {
...event.message,
content: event.message.content.slice(0, 1000) + "... (truncated)"
}
};
}
});
Event: { toolName?: string, toolCallId?: string, message: AgentMessage, isSynthetic?: boolean }
Return: { message?: AgentMessage } or void
before_message_write
Fired before writing any message to the session transcript. Can block the write.
api.registerHook("before_message_write", (event, ctx) => {
// Don't persist system messages
if (event.message.role === "system") {
return { block: true };
}
});
Event: { message: AgentMessage, sessionKey?: string, agentId?: string }
Return: { block?: boolean, message?: AgentMessage } or void
Gateway Lifecycle
gateway_start
Fired when the gateway starts.
api.registerHook("gateway_start", async (event, ctx) => {
api.logger.info(`Gateway started on port ${event.port}`);
});
Event: { port: number }
Context: { port?: number }
gateway_stop
Fired when the gateway stops.
api.registerHook("gateway_stop", async (event, ctx) => {
api.logger.info(`Gateway stopped: ${event.reason ?? "unknown"}`);
});
Event: { reason?: string }
Context: { port?: number }
Hook Files (Advanced)
OpenClaw also supports hook files with frontmatter metadata. These are typically used for bundled hooks in src/hooks/bundled/.
Hook directory structure:
hooks/
my-hook/
HOOK.md
handler.ts
HOOK.md:
---
events:
- llm_input
- llm_output
---
# My Hook
Description of what this hook does.
handler.ts:
import type { InternalHookEvent, InternalHookContext } from "openclaw/plugin-sdk";
export default async function handler(event: InternalHookEvent, ctx: InternalHookContext) {
// Handle event
}
Plugins can register hook directories:
import { registerPluginHooksFromDir } from "openclaw/plugin-sdk";
register(api: OpenClawPluginApi) {
await registerPluginHooksFromDir(api, "./hooks");
}
See src/hooks/plugin-hooks.ts for details.
Example: Session Logger
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { writeFile } from "node:fs/promises";
const plugin = {
id: "session-logger",
register(api: OpenClawPluginApi) {
const sessionLog = new Map<string, any>();
api.registerHook("session_start", async (event) => {
sessionLog.set(event.sessionId, {
startTime: Date.now(),
llmCalls: 0,
toolCalls: 0
});
});
api.registerHook("llm_input", async (event) => {
const log = sessionLog.get(event.sessionId);
if (log) log.llmCalls++;
});
api.registerHook("after_tool_call", async (event, ctx) => {
const sessionId = ctx.sessionKey;
const log = sessionId && sessionLog.get(sessionId);
if (log) log.toolCalls++;
});
api.registerHook("session_end", async (event) => {
const log = sessionLog.get(event.sessionId);
if (log) {
const report = {
sessionId: event.sessionId,
duration: Date.now() - log.startTime,
messageCount: event.messageCount,
llmCalls: log.llmCalls,
toolCalls: log.toolCalls
};
await writeFile(
`/tmp/session-${event.sessionId}.json`,
JSON.stringify(report, null, 2)
);
sessionLog.delete(event.sessionId);
}
});
}
};
export default plugin;
Next Steps
- Tools - Build custom agent tools
- Channels - Create channel integrations
- Examples - Real-world plugin examples