22.1 内部钩子(Gateway Hooks)

生成模型: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;
}

整个加载流程分两个阶段:

阶段
来源
机制
说明

阶段一

目录发现

loadWorkspaceHookEntriesshouldIncludeHook 过滤 → 动态 import()

新系统,基于 HOOK.md 文件发现

阶段二

配置文件

cfg.hooks.internal.handlers 数组

遗留系统,向后兼容

两个关键细节值得注意:

  1. 缓存清除(Cache Busting):动态导入的 URL 后附加了 ?t=${Date.now()} 时间戳参数,确保每次加载都获取最新版本的处理器代码,不会被 Node.js 的模块缓存影响。

  2. 双重禁用检查:先通过 shouldIncludeHook 做资格过滤,再检查 hookConfig?.enabled === false 做配置级禁用。两道关卡确保只有合格且启用的钩子才会被加载。

钩子发现:四源合并

钩子的发现与技能系统(第 21 章)类似,从四个目录源加载并合并:

来源
目录
来源标识
优先级

Extra

配置的额外目录

openclaw-workspace

最低

Bundled

<package>/src/hooks/bundled/

openclaw-bundled

Managed

~/.openclaw/hooks/

openclaw-managed

Workspace

<workspace>/hooks/

openclaw-workspace

最高

同名钩子遵循高优先级覆盖低优先级原则——如果工作区有一个 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 支持三种钩子安装来源:

入口函数
来源
工作方式

installHooksFromPath

本地路径

目录 → 直接复制;归档 → 解压后复制

installHooksFromArchive

归档文件

.tar.gz / .zip → 临时目录解压 → 复制

installHooksFromNpmSpec

npm 包

npm pack 下载 → 作为归档安装

所有路径最终汇聚到两个核心函数:

钩子包 vs 单个钩子

OpenClaw 区分两种安装粒度:

单个钩子——一个包含 HOOK.md + handler.ts 的目录:

钩子包——一个 npm 风格的包,可包含多个钩子:

package.json 中通过 openclaw.hooks 数组声明包含的钩子子目录:

安装流程

installHookPackageFromDir 为例,安装一个钩子包的完整流程:

路径穿越防护

安装路径的计算中内置了路径穿越(Path Traversal)检测:

衍生解释:路径穿越(Path Traversal)是一种常见的安全漏洞,攻击者通过构造包含 ../ 的路径来访问目标目录之外的文件。例如,钩子名称为 ../../etc 时,拼接后可能指向系统目录。OpenClaw 通过 path.relative() 计算相对路径,确保目标始终位于钩子安装基础目录内。

更新与回滚

钩子更新采用备份-替换-清理策略:

  1. 如果目标目录已存在,先重命名为 <dir>.backup-<timestamp>

  2. 复制新版本到目标目录

  3. 如果复制或 npm install 失败,自动从备份恢复

  4. 成功后删除备份

这种策略保证了更新的原子性——不会出现"旧版本已删除、新版本安装失败"的中间状态。


22.1.3 内置钩子(src/hooks/bundled/

OpenClaw 内置了四个钩子,覆盖了从启动初始化到会话管理的典型场景:

钩子名称
订阅事件
功能
前置条件

boot-md

gateway:startup

Gateway 启动时运行 BOOT.md 引导清单

workspace.dir 配置

session-memory

command:new

执行 /new 时保存会话到记忆文件

workspace.dir 配置

command-logger

command

记录所有命令事件到审计日志

soul-evil

agent:bootstrap

在特定条件下替换 SOUL.md 为 SOUL_EVIL.md

显式启用配置

boot-md:启动引导

boot-md 是最简单的内置钩子——它监听 gateway:startup 事件,在 Gateway 启动时自动运行工作区中的 BOOT.md 引导文件:

这个钩子的价值在于自动化——用户可以在工作区放置一个 BOOT.md 文件,定义 Gateway 启动时需要执行的准备工作(如检查依赖、初始化数据库、拉取最新配置等),每次 Gateway 重启都会自动执行。

session-memory:会话记忆保存

session-memory 是四个内置钩子中最复杂的。它在用户执行 /new 命令(开始新会话)时,将即将被丢弃的旧会话内容保存到记忆目录:

流程分三步:

  1. 提取会话内容:从 JSONL 格式的会话文件中解析最近 N 条 user/assistant 消息(跳过以 / 开头的命令消息)。

  2. LLM 生成文件名:调用配置好的 LLM 提供者,基于会话内容生成一个 1-2 个单词的文件名别名(slug),如 vendor-pitchapi-design。这通过 generateSlugViaLLM 函数实现,它会启动一个临时的嵌入式 Agent 来完成这个小任务。如果 LLM 调用失败或在测试环境中,则回退到时间戳格式 HHMM

  3. 保存记忆文件:在 <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:人格替换

soul-evil 是一个趣味性与实验性兼具的钩子——它可以在特定条件下将 Agent 的系统人格文件 SOUL.md 替换为 SOUL_EVIL.md,让 Agent 表现出完全不同的"人格"。

触发条件有两种:

  1. 净化窗口(Purge Window):配置每天一个固定时间段(如 21:00-21:15),在此期间所有请求使用 SOUL_EVIL.md。

  2. 随机概率:配置 0-1 之间的概率值,每次请求按概率随机触发。

这个钩子默认不启用——它的元数据中声明了 requires.config: ["hooks.internal.entries.soul-evil.enabled"],只有用户显式在配置中设置 enabled: true 才会激活。


22.1.4 agent:bootstrap 钩子

agent:bootstrap 是内部钩子系统中最强大的事件——它在 Agent 构建系统提示词之前触发,允许钩子修改 Agent 的引导文件列表。

事件结构

与其他事件不同,agent:bootstrapcontext 中包含了 bootstrapFiles 数组——这是 Agent 构建系统提示词时使用的工作区文件列表(如 SOUL.mdAGENTS.mdBOOT.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)

注册与触发

注册时可以订阅两种粒度的事件键:

触发时,系统会同时调用两种订阅的处理器:

这种设计有两个重要特性:

  1. 双层订阅command 可以捕获所有命令事件(如 command-logger 的审计需求),command:new 只捕获特定命令(如 session-memory 的精确触发)。

  2. 错误隔离:每个处理器的执行被 try/catch 包裹,一个钩子抛出异常不会中断其他钩子的执行。这对于生产环境至关重要——不能因为一个第三方钩子的 bug 导致整个命令处理流程崩溃。

命令事件的生命周期

当用户执行 /new 命令时,系统会创建并触发一个命令事件:

对于这个事件,以下钩子会被触发:

钩子
订阅键
匹配方式

command-logger

command

类型匹配(所有命令都会触发)

session-memory

command:new

动作匹配(只有 /new 触发)

消息回推

InternalHookEvent 中的 messages 数组是一个双向通信机制——钩子可以向这个数组推送消息,这些消息会被返回给用户:

这个机制让钩子不仅能执行后台操作,还能向用户反馈执行结果——虽然目前内置的钩子选择了静默运行(不推送用户可见的消息),但这个能力为自定义钩子提供了丰富的交互空间。


本节小结

  1. 内部钩子是 OpenClaw 的事件驱动扩展机制,基于发布/订阅模式实现,处理器注册到事件键上,事件触发时按注册顺序依次调用。

  2. 钩子加载分两阶段:目录发现(新系统)和配置处理器(遗留兼容),从 extra/bundled/managed/workspace 四个源加载并按优先级合并。

  3. 钩子安装支持本地路径、归档文件和 npm 包三种来源,内置路径穿越防护和备份回滚机制。

  4. 四个内置钩子覆盖了启动引导(boot-md)、会话记忆(session-memory)、命令审计(command-logger)和人格替换(soul-evil)四个场景。

  5. agent:bootstrap 事件是最强大的扩展点,允许钩子在系统提示词构建前修改引导文件列表,实现对 Agent 行为的动态控制。

  6. 事件模型采用 type + action 二维键,支持类型级和动作级两种粒度的订阅,并通过 try/catch 实现错误隔离,保证系统稳定性。

Last updated