生成模型:Claude Opus 4.6 (anthropic/claude-opus-4-6) Token 消耗:输入 ~120k tokens,输出 ~8k tokens(本节)
OpenClaw 的钩子系统(Hook System)是一套事件驱动的扩展机制,允许在 Agent 生命周期的关键节点注入自定义逻辑——无需修改核心代码。本节聚焦内部钩子(Internal Hooks),即运行在 Gateway 进程内部、通过事件注册/触发的钩子体系。
衍生解释:钩子(Hook)是软件工程中的经典模式。它在程序执行流的某些"节点"预留了扩展点,让外部代码可以在这些节点上"挂钩"自己的逻辑。React 的生命周期方法、Git 的 pre-commit/post-commit、Webpack 的 tapable 插件系统,都是钩子模式的不同实现。OpenClaw 的钩子采用的是发布/订阅(Pub/Sub)变体——注册处理器监听事件键,事件触发时按注册顺序依次调用。
22.1.1 钩子加载器(src/hooks/loader.ts)
loadInternalHooks 是钩子系统的入口函数。它在 Gateway 启动时被调用,负责发现、过滤、导入并注册所有内部钩子处理器。
// src/hooks/loader.ts(简化)
export async function loadInternalHooks(
cfg: OpenClawConfig,
workspaceDir: string,
): Promise<number> {
// 前置检查:钩子系统是否启用
if (!cfg.hooks?.internal?.enabled) {
return 0;
}
let loadedCount = 0;
// 阶段一:从目录发现钩子(新系统)
const hookEntries = loadWorkspaceHookEntries(workspaceDir, { config: cfg });
const eligible = hookEntries.filter(entry =>
shouldIncludeHook({ entry, config: cfg })
);
for (const entry of eligible) {
// 跳过配置中显式禁用的钩子
const hookConfig = resolveHookConfig(cfg, entry.hook.name);
if (hookConfig?.enabled === false) continue;
// 动态导入处理器模块(带缓存清除)
const url = pathToFileURL(entry.hook.handlerPath).href;
const mod = await import(`${url}?t=${Date.now()}`);
// 获取导出的处理器函数
const exportName = entry.metadata?.export ?? "default";
const handler = mod[exportName];
// 为元数据中声明的每个事件注册处理器
const events = entry.metadata?.events ?? [];
for (const event of events) {
registerInternalHook(event, handler);
}
loadedCount++;
}
// 阶段二:加载遗留配置处理器(向后兼容)
const handlers = cfg.hooks.internal.handlers ?? [];
for (const handlerConfig of handlers) {
const modulePath = path.isAbsolute(handlerConfig.module)
? handlerConfig.module
: path.join(process.cwd(), handlerConfig.module);
const mod = await import(pathToFileURL(modulePath).href);
registerInternalHook(handlerConfig.event, mod[handlerConfig.export ?? "default"]);
loadedCount++;
}
return loadedCount;
}
整个加载流程分两个阶段:
loadWorkspaceHookEntries → shouldIncludeHook 过滤 → 动态 import()
cfg.hooks.internal.handlers 数组
两个关键细节值得注意:
缓存清除(Cache Busting):动态导入的 URL 后附加了 ?t=${Date.now()} 时间戳参数,确保每次加载都获取最新版本的处理器代码,不会被 Node.js 的模块缓存影响。
双重禁用检查:先通过 shouldIncludeHook 做资格过滤,再检查 hookConfig?.enabled === false 做配置级禁用。两道关卡确保只有合格且启用的钩子才会被加载。
钩子的发现与技能系统(第 21 章)类似,从四个目录源加载并合并:
<package>/src/hooks/bundled/
同名钩子遵循高优先级覆盖低优先级原则——如果工作区有一个 session-memory 钩子,它会覆盖内置的同名钩子。
每个钩子是一个目录,至少包含两个文件:
HOOK.md 的 Front Matter 定义了钩子的元数据:
resolveOpenClawMetadata 负责解析 Front Matter 中 metadata 字段下的 openclaw 对象,提取出 OpenClawHookMetadata:
shouldIncludeHook 对每个钩子进行多维度的资格检查:
这套过滤机制与技能系统的 shouldIncludeSkill(第 21.1 节)高度对称——相同的设计哲学,钩子能用的检查维度和技能一致。这意味着钩子可以声明"我只在 macOS 上运行"、"我需要 ffmpeg 命令"、"我需要 OPENAI_API_KEY 环境变量"等前提条件。
22.1.2 钩子安装(src/hooks/install.ts)
OpenClaw 支持三种钩子安装来源:
.tar.gz / .zip → 临时目录解压 → 复制
所有路径最终汇聚到两个核心函数:
OpenClaw 区分两种安装粒度:
单个钩子——一个包含 HOOK.md + handler.ts 的目录:
钩子包——一个 npm 风格的包,可包含多个钩子:
package.json 中通过 openclaw.hooks 数组声明包含的钩子子目录:
以 installHookPackageFromDir 为例,安装一个钩子包的完整流程:
安装路径的计算中内置了路径穿越(Path Traversal)检测:
衍生解释:路径穿越(Path Traversal)是一种常见的安全漏洞,攻击者通过构造包含 ../ 的路径来访问目标目录之外的文件。例如,钩子名称为 ../../etc 时,拼接后可能指向系统目录。OpenClaw 通过 path.relative() 计算相对路径,确保目标始终位于钩子安装基础目录内。
钩子更新采用备份-替换-清理策略:
如果目标目录已存在,先重命名为 <dir>.backup-<timestamp>
如果复制或 npm install 失败,自动从备份恢复
这种策略保证了更新的原子性——不会出现"旧版本已删除、新版本安装失败"的中间状态。
22.1.3 内置钩子(src/hooks/bundled/)
OpenClaw 内置了四个钩子,覆盖了从启动初始化到会话管理的典型场景:
Gateway 启动时运行 BOOT.md 引导清单
在特定条件下替换 SOUL.md 为 SOUL_EVIL.md
boot-md 是最简单的内置钩子——它监听 gateway:startup 事件,在 Gateway 启动时自动运行工作区中的 BOOT.md 引导文件:
这个钩子的价值在于自动化——用户可以在工作区放置一个 BOOT.md 文件,定义 Gateway 启动时需要执行的准备工作(如检查依赖、初始化数据库、拉取最新配置等),每次 Gateway 重启都会自动执行。
session-memory:会话记忆保存
session-memory 是四个内置钩子中最复杂的。它在用户执行 /new 命令(开始新会话)时,将即将被丢弃的旧会话内容保存到记忆目录:
流程分三步:
提取会话内容:从 JSONL 格式的会话文件中解析最近 N 条 user/assistant 消息(跳过以 / 开头的命令消息)。
LLM 生成文件名:调用配置好的 LLM 提供者,基于会话内容生成一个 1-2 个单词的文件名别名(slug),如 vendor-pitch、api-design。这通过 generateSlugViaLLM 函数实现,它会启动一个临时的嵌入式 Agent 来完成这个小任务。如果 LLM 调用失败或在测试环境中,则回退到时间戳格式 HHMM。
保存记忆文件:在 <workspace>/memory/ 目录下创建 YYYY-MM-DD-slug.md 文件,内容包含会话键、会话 ID、来源信息和对话摘要。
command-logger:命令审计日志
command-logger 订阅所有 command 类型事件(注意不是特定的 command:new,而是通配的 command),将每次命令执行以 JSONL 格式追加到日志文件:
日志文件位于 ~/.openclaw/logs/commands.log,每行是一个独立的 JSON 对象,方便用 jq 等工具分析:
soul-evil 是一个趣味性与实验性兼具的钩子——它可以在特定条件下将 Agent 的系统人格文件 SOUL.md 替换为 SOUL_EVIL.md,让 Agent 表现出完全不同的"人格"。
触发条件有两种:
净化窗口(Purge Window):配置每天一个固定时间段(如 21:00-21:15),在此期间所有请求使用 SOUL_EVIL.md。
随机概率:配置 0-1 之间的概率值,每次请求按概率随机触发。
这个钩子默认不启用——它的元数据中声明了 requires.config: ["hooks.internal.entries.soul-evil.enabled"],只有用户显式在配置中设置 enabled: true 才会激活。
22.1.4 agent:bootstrap 钩子
agent:bootstrap 是内部钩子系统中最强大的事件——它在 Agent 构建系统提示词之前触发,允许钩子修改 Agent 的引导文件列表。
与其他事件不同,agent:bootstrap 的 context 中包含了 bootstrapFiles 数组——这是 Agent 构建系统提示词时使用的工作区文件列表(如 SOUL.md、AGENTS.md、BOOT.md 等)。钩子可以直接修改这个数组,从而影响 Agent 的行为。
由于 InternalHookEvent.context 的类型是宽泛的 Record<string, unknown>,OpenClaw 提供了类型守卫函数来安全地收窄类型:
衍生解释:TypeScript 的类型守卫(Type Guard)是一种通过运行时检查来收窄类型的机制。event is AgentBootstrapHookEvent 是一个类型谓词(Type Predicate)——当函数返回 true 时,TypeScript 编译器会将 event 的类型从 InternalHookEvent 收窄为 AgentBootstrapHookEvent,从而允许安全地访问 context.bootstrapFiles 等字段。
实际应用:soul-evil 的引导文件替换
soul-evil 钩子是 agent:bootstrap 事件的典型消费者。它的核心逻辑在 applySoulEvilOverride 中:
注意这里的关键设计:钩子不修改磁盘上的文件,只是替换了内存中引导文件列表里 SOUL.md 条目的 content 字段。这意味着替换是临时的——只影响当前请求的系统提示词构建,不会永久改变文件。
22.1.5 命令钩子:/new、/reset、/stop
内部钩子的事件系统基于一个简洁的二维模型:类型(type)+ 动作(action)。
注册时可以订阅两种粒度的事件键:
触发时,系统会同时调用两种订阅的处理器:
这种设计有两个重要特性:
双层订阅:command 可以捕获所有命令事件(如 command-logger 的审计需求),command:new 只捕获特定命令(如 session-memory 的精确触发)。
错误隔离:每个处理器的执行被 try/catch 包裹,一个钩子抛出异常不会中断其他钩子的执行。这对于生产环境至关重要——不能因为一个第三方钩子的 bug 导致整个命令处理流程崩溃。
当用户执行 /new 命令时,系统会创建并触发一个命令事件:
对于这个事件,以下钩子会被触发:
InternalHookEvent 中的 messages 数组是一个双向通信机制——钩子可以向这个数组推送消息,这些消息会被返回给用户:
这个机制让钩子不仅能执行后台操作,还能向用户反馈执行结果——虽然目前内置的钩子选择了静默运行(不推送用户可见的消息),但这个能力为自定义钩子提供了丰富的交互空间。
内部钩子是 OpenClaw 的事件驱动扩展机制,基于发布/订阅模式实现,处理器注册到事件键上,事件触发时按注册顺序依次调用。
钩子加载分两阶段:目录发现(新系统)和配置处理器(遗留兼容),从 extra/bundled/managed/workspace 四个源加载并按优先级合并。
钩子安装支持本地路径、归档文件和 npm 包三种来源,内置路径穿越防护和备份回滚机制。
四个内置钩子覆盖了启动引导(boot-md)、会话记忆(session-memory)、命令审计(command-logger)和人格替换(soul-evil)四个场景。
agent:bootstrap 事件是最强大的扩展点,允许钩子在系统提示词构建前修改引导文件列表,实现对 Agent 行为的动态控制。
事件模型采用 type + action 二维键,支持类型级和动作级两种粒度的订阅,并通过 try/catch 实现错误隔离,保证系统稳定性。