前置阅读:第 1 篇 — Plugin SDK 入门
本篇讲解 OpenClaw 的事件钩子系统。钩子是插件介入 Agent 生命周期的核心机制——通过钩子,你可以在工具调用前拦截、在消息发送前修改、在会话绑定后执行自定义逻辑,而不需要修改核心代码。
01
—
钩子系统概述
钩子(Hook)的本质是在特定事件发生时自动执行的回调函数。它是一种观察者模式的实现,但带有更精细的控制语义(后面会详细讲)。
打个比方:如果 OpenClaw 的运行是一条流水线,那么钩子就是在流水线的特定工位上安装的「质检站」——每当产品经过这个工位,质检站就自动工作,可以检查产品、修改产品,甚至拦截不让产品继续往下走。
OpenClaw 的钩子系统有两个注册入口,都在 register(api) 回调中通过 api 对象调用:
// 方式 1:通过 api.registerHook 注册api.registerHook("event_name", handler, options?);// 方式 2:通过 api.on 注册(语义等价)api.on("event_name", handler, options?);
两种方式功能一致,选哪个都可以。api.on 写法更简洁,api.registerHook 语义更显式。本篇后续示例统一使用 api.on。
—
钩子优先级
在多插件环境下,可能有多个插件对同一个事件注册钩子。例如,一个安全插件和一个审计插件都注册了before_tool_call。这时,执行顺序由优先级控制——优先级高的先执行:api.on("before_tool_call", handler, {priority: 10, // 数字越大,优先级越高,越先执行});
默认优先级为 0。高优先级处理器先执行,低优先级后执行。如果高优先级处理器做出了终局性决策,低优先级处理器将被跳过。
03
—
核心钩子详解
before_tool_call
触发时机:Agent 决定调用某个工具,但在实际执行之前。这是插件介入工具执行的最后机会。
用途:
审计和日志记录
安全检查和权限验证
参数修改或注入
阻止特定工具的执行
api.on("before_tool_call", async (context) => {// context 包含本次工具调用的详细信息const { toolName, parameters, conversationId } = context;// toolName: 工具名称(如 "exec"、"web_search")// parameters: Agent 传给工具的参数// conversationId: 当前会话 ID// 记录审计日志api.logger.info(`Tool call: ${toolName}`, { parameters });// 示例:阻止对危险工具的调用if (toolName === "exec" && parameters.command.includes("rm -rf")) {return { block: true }; // 返回 block: true 表示终局性阻止}// 示例:修改参数——强制开启安全搜索if (toolName === "web_search" && !parameters.safeSearch) {parameters.safeSearch = true; // 直接修改 parameters 对象即可}// 不返回 block,或返回 { block: false } = 不干预,工具正常执行});
决策语义:

注意:{ block: false }不是「允许调用」的意思,而是「我不做决定,让下一个处理器来」。如果你只想观察而不干预,不返回 block 字段即可。
message_sending
触发时机:Agent 生成回复后,消息发送到通道之前。
用途:
内容过滤和合规检查
消息格式转换
添加额外的上下文信息
阻止不当消息的发送
api.on("message_sending", async (context) => {const { message, channel, conversationId } = context;// 示例:敏感词过滤const sensitiveWords = ["机密", "绝密", "内部文件"];for (const word of sensitiveWords) {if (message.text?.includes(word)) {api.logger.warn(`Blocked message containing: ${word}`, {conversationId,});return { cancel: true }; // 终局性阻止}}// 示例:为回复添加来源标注if (message.text) {message.text += "\n\n_此回复由 AI 助手生成_";}// 继续传播return { cancel: false };});
决策语义:

onConversationBindingResolved
触发时机:会话绑定解析完成时。「会话绑定」是指 OpenClaw 确定某个对话应该使用哪个 Agent、哪个 Channel 的过程。这个钩子在实际开发中较少用到,一般用于需要基于会话上下文做初始化的场景。
用途:
会话级别的初始化
基于会话上下文的配置加载
会话级别的状态管理
api.onConversationBindingResolved(async (binding) => {// binding 包含会话绑定信息api.logger.info(`Conversation resolved: ${binding.conversationId}, ` +`agent: ${binding.agentId}, channel: ${binding.channelId}`);});
04
—
钩子链传播机制

关键规则:
处理器按优先级从高到低依次执行
如果某个处理器返回终局性决策(
block: true或cancel: true),传播立即停止block: false和cancel: false不是「允许」,而是「不干预」如果所有处理器都不做决策,默认行为继续执行(工具正常调用 / 消息正常发送)
这个设计避免了「一个插件阻止,另一个插件又允许」的歧义——第一个做出终局性决策的处理器说了算。
05
—
钩子的常见模式
模式 1:纯观察(审计日志)
api.on("before_tool_call", async (context) => {api.logger.info("Tool call audit", {tool: context.toolName,params: JSON.stringify(context.parameters),timestamp: new Date().toISOString(),});// 不返回任何决策字段 = 纯观察});
模式 2:条件拦截
api.on("before_tool_call", async (context) => {if (context.toolName === "exec") {const cmd = context.parameters.command as string;const dangerous = ["sudo", "rm -rf", "format", "del /S"];if (dangerous.some(d => cmd.includes(d))) {api.logger.warn(`Blocked dangerous command: ${cmd}`);return { block: true };}}});
模式 3:参数注入
api.on("before_tool_call", async (context) => {// 为所有工具调用添加审计标记context.parameters._auditId = generateAuditId();context.parameters._timestamp = Date.now();// 不阻止,仅修改参数});
模式 4:消息后处理
api.on("message_sending", async (context) => {if (context.message.text) {// 统一格式化消息context.message.text = formatResponse(context.message.text);// 截断过长的消息if (context.message.text.length > 4000) {context.message.text =context.message.text.substring(0, 3997) + "...";}}});
06
—
钩子 vs 工具

选择原则:
如果是「每次都应该执行」的逻辑 → 钩子
如果是「Agent 需要自主决策是否使用」的能力 → 工具
例如:审计日志适合用钩子(每次工具调用都要记录),而天气查询适合用工具(Agent 根据对话内容决定是否调用)。
07
—
完整示例:安全审计插件
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";export default definePluginEntry({id: "audit-plugin",name: "Audit Plugin",version: "1.0.0",register(api) {// 工具调用审计api.on("before_tool_call", async (context) => {const entry = {event: "tool_call",tool: context.toolName,params: sanitizeForLog(context.parameters),conversationId: context.conversationId,timestamp: new Date().toISOString(),};// 记录到日志api.logger.info("AUDIT", entry);// 危险操作拦截if(isDangerousOperation(context.toolName, context.parameters)) {api.logger.warn("BLOCKED dangerous operation", entry);return { block: true };}});// 消息发送审计api.on("message_sending", async (context) => {const entry = {event: "message_sending",channel: context.channel,textLength: context.message.text?.length ?? 0,hasAttachments: (context.message.attachments?.length ?? 0) > 0,conversationId: context.conversationId,timestamp: new Date().toISOString(),};api.logger.info("AUDIT", entry);// 内容合规检查if(containsProhibitedContent(context.message.text ?? "")) {api.logger.warn("BLOCKED prohibited content", entry);return { cancel: true };}});// 会话绑定跟踪api.onConversationBindingResolved(async (binding) => {api.logger.info("AUDIT", {event: "conversation_resolved",conversationId: binding.conversationId,agentId: binding.agentId,channelId: binding.channelId,});});},});functionisDangerousOperation(toolName: string, params: Record<string, unknown>): boolean{if(toolName === "exec") {const cmd = String(params.command ?? "");return ["/sudo", "rm -rf", "mkfs", "format "].some(d => cmd.includes(d));}return false;}functioncontainsProhibitedContent(text: string): boolean{const prohibited = ["[PROHIBITED_PATTERN_1]", "[PROHIBITED_PATTERN_2]"];return prohibited.some(p => text.includes(p));}functionsanitizeForLog(params: Record<string, unknown>): Record<string, unknown> {// 移除敏感参数const sanitized = { ...params };if("apiKey" in sanitized) sanitized.apiKey = "***";if("token" in sanitized) sanitized.token = "***";return sanitized;}
08
—
小结
本篇覆盖了 OpenClaw 钩子系统的完整机制:
钩子是插件介入 Agent 生命周期的核心机制
两个注册方式:
api.registerHook和api.on优先级控制多处理器的执行顺序
before_tool_call和message_sending的终局性决策语义四种常见模式:纯观察、条件拦截、参数注入、消息后处理
钩子适合横切关注点,工具适合业务能力
下一篇:上下文引擎 — 控制 Agent 的「视野」。我们将学习如何通过自定义上下文引擎精确控制 Agent 在每次对话中能「看到」什么信息。
夜雨聆风