OpenClaw hooks 怎么用:事件清单、目录结构与"装了不跑"的三种原因

2026-08-17

想让 OpenClaw 在你敲 /new 的时候自动存一份会话快照,或者在消息发出去之后顺手调一个外部接口,官方给的路子是 hooks——跑在网关(Gateway)进程里的小脚本,agent 事件一触发就执行。

麻烦在于 hooks 这个词在 OpenClaw 里指的东西不止一种,而且它默认压根不加载。文档里写得很直白:网关只有在你启用了 hooks,或者至少配置了一个 hook 条目、一个 hook pack、一个额外 hook 目录之后,才会去加载内部 hook。也就是说你把目录建好、handler 写完、重启网关,什么都不会发生——因为网关在启动阶段就跳过了内部 hook 的发现流程。这是最常见的”写完不跑”。

下面按官方文档把这套机制拆开讲:先分清几个长得很像的扩展面,再说事件、目录结构、发现优先级和配置,最后是排查路径和适用边界。

先分清:内部 hooks、webhook、插件类型化 hook 是三件事

OpenClaw 文档特意把这三个摆在一起对比过,因为它们看起来都叫 hook,解决的却不是同一类问题。

你想做的事该用的东西文档给的理由
/new 时存快照、记录 /resetmessage:sent 之后调外部 API,或者加一些粗粒度的运维自动化内部 hooks(HOOK.md,本文讲的这套)基于文件的 hook 就是为运维侧的副作用和命令/生命周期自动化设计的
改写提示词、拦截工具调用、取消外发消息、加有序的中间件或策略插件里的类型化 hook,用 api.on(...) 注册类型化 hook 有明确契约、优先级、合并规则和阻断/取消语义
只做遥测导出或可观测性诊断事件(diagnostic events)可观测性走的是另一条事件总线,不是策略 hook 面

另外还有 webhook,那是外部 HTTP 端点,让别的系统反过来触发 OpenClaw 干活,跟本文说的内部 hooks 是两回事,官方文档单独有一页讲它。

一句话判断标准:想要”像装了个小集成一样”的自动化,用内部 hooks;需要在运行时控制生命周期、拦下点什么,用插件的类型化 hook。

还有一条容易忽略的约束:内部 hook 的 handler 是请求/事件处理器,不能自己持有长期存活的定时器、watcher、socket 或客户端。真需要常驻的东西,官方要求插件去注册一个 service,或者用类型化的 gateway_start / gateway_stop 生命周期。

事件键:十五个,写错一个字就静默失效

hook 可以订阅某个具体的事件键,也可以订阅一个”家族名”(commandsessionagentgatewaymessage),订阅家族名就能收到该家族下的所有动作。

事件触发时机
command:new发出 /new 命令
command:reset发出 /reset 命令
command:stop发出 /stop 命令
command任意命令事件(通用监听)
session:auto-reset每日重置或空闲重置替换了当前会话
session:compact:before压缩开始总结历史之前
session:compact:after压缩完成之后
session:patch会话属性被修改时
agent:bootstrap工作区 bootstrap 文件注入之前
gateway:startup渠道启动、hooks 加载完成之后
gateway:shutdown网关开始关闭时
gateway:pre-restart预期内的网关重启之前
message:received任意渠道收到入站消息
message:transcribed音频转写完成之后
message:preprocessed媒体与链接预处理完成或被跳过之后
message:sent尝试外发消息(结果在 context.success 里)

OpenClaw 核心只发这些,别的名字基本都是拼错。好消息是这个错不算完全无声:hook 加载器会为这类名字打一条警告日志(文档举的例子是 command:nwe),openclaw hooks info <name> 也会把它标出来,所以一个从来不跑的 hook 是可以诊断出来的。理论上只有插件自己发的自定义事件才会命中非核心名字。

每个事件对象都带 typeactionsessionKeytimestampmessagescontext(各事件自己的数据)。context 里具体有什么,文档按事件分别列了,挑几个常用的:

  • command:new / command:resetcontext.sessionEntrycontext.previousSessionEntrycontext.commandSourcecontext.senderIdcontext.workspaceDircontext.cfg
  • session:auto-reset:多一个 context.reason,取值是 dailyidle,还有 context.transcriptArchivedcontext.nextSessionIdcontext.agentIdcontext.storePath 等。
  • message:receivedcontext.fromcontext.contentcontext.channelIdcontext.media,以及 context.metadata(渠道方提供的数据,含 senderIdsenderNameguildId)。要注意 context.content 不含 agent 侧的富化内容,比如线程历史和链接摘要。
  • message:sentcontext.tocontext.contentcontext.successcontext.channelId,发送失败时还有 context.error
  • 压缩事件:beforemessageCounttokenCountafter 再加 compactedCountsummaryLengthtokensBeforetokensAfter。想理解这几个数字对应的是什么过程,可以看会话压缩机制那篇

session:patch 有个安全设计值得单独说:只有特权客户端能触发 patch 事件,而且传给 handler 的 context 是克隆出来的,handler 改不动活着的那份会话条目。

event.messages 推进去的字符串,多数事件根本不会送出去

这条是我看文档时觉得最容易造成误会的地方。handler 里可以往 event.messages 里 push 字符串,看起来像是”给聊天窗口回一句话”:

const handler = async (event) => {
  if (event.type !== "command" || event.action !== "new") {
    return;
  }

  console.log(`[my-hook] New command triggered`);
  // Your logic here

  // Optionally send a reply on replyable surfaces
  event.messages.push("Hook executed!");
};

export default handler;

但实际会被送回聊天的只有两类:command:newcommand:reset(作为回复路由到原会话),以及 session:compact:before / session:compact:after(作为压缩状态提示发出)。其余全部忽略——包括 command:stop、所有 message:*agent:bootstrapsession:patchgateway:*。所以你在 message:received 里 push 半天没反应,不是 bug。

另外文档明确划了一条线:command:stop 观察的是用户发出 /stop 这个动作,属于取消/命令生命周期,不是 agent 最终作答的闸门。插件如果想检查一个自然的最终答复、再让 agent 多跑一轮,该用类型化插件 hook before_agent_finalize

目录结构:一个 hook 就是两个文件

my-hook/
├── HOOK.md          # Metadata + documentation
└── handler.ts       # Handler implementation

handler 文件名可以是 handler.tshandler.jsindex.tsindex.js 四选一。HOOK.md 是带 frontmatter 的说明文件:

---
name: my-hook
description: "Short description of what this hook does"
metadata:
  { "openclaw": { "emoji": "🔗", "events": ["command:new"], "requires": { "bins": ["node"] } } }
---

# My Hook

Detailed documentation goes here.

metadata.openclaw 里可用的字段:emoji(CLI 里显示的表情)、events(监听的事件数组)、export(用哪个具名导出,默认 "default")、os(要求的平台,如 ["darwin", "linux"])、requires(要求的 binsanyBinsenv 或 config 路径)、always(在兼容 OS 上跳过 requires.* 检查)、hookKey(配置键覆盖,默认用 hook 名)、homepageopenclaw hooks info 里显示的文档地址)、install(安装方式)。

官方在”最佳实践”里给了四条,都是很实在的工程约束:handler 要快,因为 hook 是在命令处理过程中跑的,重活用 void processInBackground(event) 发射后不管;risky 的操作包 try/catch,不要往外抛异常,否则其他 handler 没法跑;不相关的事件类型尽早 return;事件键尽量写具体的 ["command:new"] 而不是 ["command"],减少开销。

四个来源、有覆盖优先级

hook 从四个地方被发现,顺序本身就是覆盖规则:

  1. Bundled hooks:跟 OpenClaw 一起发布的内置 hook。
  2. Plugin hooks:装在插件里的,可以用同名覆盖 bundled。
  3. Managed hooks~/.openclaw/hooks/,用户安装、跨工作区共享,可以覆盖 bundled 和 plugin。hooks.internal.load.extraDirs 配的额外目录跟它同一优先级。
  4. Workspace hooks<workspace>/hooks/,按 agent 走,默认关闭直到显式启用。

工作区 hook 只能加新名字,不能用同名覆盖 bundled、managed 或插件提供的 hook。想理解这套按 agent 隔离的边界,插件体系那篇讲得更细,可以对照着看 OpenClaw 插件体系

回到开头那个开关:网关在启动时会跳过内部 hook 发现,直到内部 hook 被配置过。三种方式可以打开——用 openclaw hooks enable <name> 启用一个 bundled 或 managed hook、装一个 hook pack、或者直接设 hooks.internal.enabled=true。注意即使主开关是 true,具名条目仍然是一份白名单;只有裸的 hooks.internal.enabled=true(没有任何具名条目)才会开启广泛发现。非空的额外目录、以及没有声明 hook 名字的 hook pack 安装,同样属于”开放式”的情形。

hook pack 是通过 package.json 里的 openclaw.hooks 导出 hook 的 npm 包:

openclaw plugins install <path-or-spec>

npm spec 只认注册表来源(包名 + 可选的精确版本或 dist-tag),git/URL/file spec 和 semver 范围会被拒绝。老的 openclaw hooks installopenclaw hooks update 现在是 openclaw plugins install / openclaw plugins update 的废弃别名。

五个内置 hook,先看看有没有现成的

Hook事件作用
session-memorycommand:newcommand:resetsession:auto-reset把会话上下文存到 <workspace>/memory/
bootstrap-extra-filesagent:bootstrap按 glob 模式注入额外的 bootstrap 文件
command-loggercommand把所有命令记到 ~/.openclaw/logs/commands.log
compaction-notifiersession:compact:beforesession:compact:after压缩开始/结束时在聊天里发可见提示
boot-mdgateway:startup网关启动时运行 BOOT.md

session-memory 是里面配置项最多的一个:在 /new/reset、每日重置或空闲过期时,抽取最后若干条用户/助手消息(默认 15 条,用 hooks.internal.entries.session-memory.messages 改),按 agents.defaults.userTimezone 存成 <workspace>/memory/YYYY-MM-DD-HHMM.md;没配用户时区就退回主机时区。记忆抓取跑在后台,所以重置处理和替换会话不会被读取记录稿拖慢。想让文件名带描述性 slug 就设 llmSlug: true,还能用 model 指定一个已配置的别名(比如 sonnet)、默认供应商上的裸 model ID,或者 provider/model 形式的引用;省略 model 时用 agent 的默认模型,取不到就退回时间戳 slug。这个 hook 要求 workspace.dir 已配置。

官方在这里挂了一条提醒:memory 源已经会索引这个 hook 存下的对话摘录,如果会话记录稿索引也开着,同一段对话会同时从 memorysessions 两边出现,造成搜索结果重叠和额外的向量化开销。只想要 hook 这一份的话,设 memory.search.sources: ["memory"]memory.search.rememberAcrossConversations: false——光设 sources 挡不住跨对话召回把 sessions 又加回来。反过来想要全量记录稿召回,就 openclaw hooks disable session-memory。两边的取舍逻辑,记忆架构那篇里有更完整的背景。

bootstrap-extra-files 的配置长这样:

{
  "hooks": {
    "internal": {
      "entries": {
        "bootstrap-extra-files": {
          "enabled": true,
          "paths": ["packages/*/AGENTS.md"]
        }
      }
    }
  }
}

patternsfilespaths 的别名。路径相对工作区解析,且必须留在工作区内。只有被认可的 bootstrap 基名会被加载:AGENTS.mdSOUL.mdIDENTITY.mdUSER.mdBOOTSTRAP.mdMEMORY.mdTOOLS.md 已经不在这个名单里,不会被载入运行时上下文;openclaw doctor --fix 会把工作区根目录的 TOOLS.md 迁进 AGENTS.md## Tools 小节,但指向其他位置 TOOLS.md 的模式不会被迁移,需要自己改指到 AGENTS.md

配置块与一条退休警告

主配置结构:

{
  "hooks": {
    "internal": {
      "enabled": true,
      "entries": {
        "session-memory": { "enabled": true },
        "command-logger": { "enabled": false }
      }
    }
  }
}

每个 hook 可以带自己的环境变量,这些值既能满足 requires.env 的资格检查(和进程环境并列),handler 也能从自己的配置条目里读到:

{
  "hooks": {
    "internal": {
      "entries": {
        "my-hook": {
          "enabled": true,
          "env": { "MY_CUSTOM_VAR": "value" }
        }
      }
    }
  }
}

额外目录用 hooks.internal.load.extraDirs 配一个路径数组。

需要单独留意的是:hooks.internal.handlers 已经退休,正常的配置校验不再加载也不再接受它。跑 openclaw doctor --fix 之前,先把每个注册过的模块搬到 managed 或 workspace hook 目录里,配上 HOOK.md 和 handler 文件——doctor 只会删掉这些退休的注册项,不会帮你生成可执行的 hook 文件。如果是只剩遗留配置、且 hooks.internal.enabled: true 的情况,doctor 还会把 enabled 一并删掉,免得意外启用了别的被发现的 hook。规范条目、非空的额外目录、以及显式的 enabled: false 会被保留。

排查:按”没发现 / 没资格 / 没执行”三段走

CLI 就这几条:

# 列出全部 hook(可加 --eligible、--verbose、--json)
openclaw hooks list

# 看某个 hook 的详情
openclaw hooks info <hook-name>

# 看资格检查汇总
openclaw hooks check

# 启用/停用
openclaw hooks enable <hook-name>
openclaw hooks disable <hook-name>

没被发现:先确认目录结构对不对,ls -la ~/.openclaw/hooks/my-hook/ 应该能看到 HOOK.mdhandler.ts,然后 openclaw hooks list 看它在不在列表里。顺带一提,openclaw hooks list 同时会列出独立 hook 和插件管理的 hook,后者显示成 plugin:<id>

没资格openclaw hooks info my-hook,然后对着看缺的是哪一项——二进制没在 PATH 里、环境变量没设、配置值缺失,还是 OS 不兼容。

没执行:三步。确认 hook 是启用状态(openclaw hooks list);重启网关进程让 hook 重新加载;查网关日志 openclaw logs --follow | grep -i hook。日志这条线的更多开关,见网关诊断与日志

关于网关重启:两个事件和一段 drain

gateway:shutdown 的 context 带 reasonrestartExpectedMs,在关闭开始时触发;gateway:pre-restart context 相同,但只在关闭属于预期内重启、且给了有限的 restartExpectedMs 值时才触发。关闭过程中每个生命周期 hook 的等待都是尽力而为且有上限的,handler 卡住不会拖住关闭流程——默认预算是 gateway:shutdown 5 秒、gateway:pre-restart 10 秒。

文档给的典型用法是在渠道还活着的时候发一条短的重启通知:

import { execFile } from "node:child_process";
import { promisify } from "node:util";

const execFileAsync = promisify(execFile);

export default async function handler(event) {
  if (event.type !== "gateway" || event.action !== "pre-restart") {
    return;
  }

  const restartInSeconds = Math.ceil(event.context.restartExpectedMs / 1000);
  await execFileAsync("openclaw", [
    "system",
    "event",
    "--mode",
    "now",
    "--text",
    `Gateway restarting in ~${restartInSeconds}s (${event.context.reason}). Checkpoint now.`,
  ]);
}

在这两个事件和后续关闭序列之间,网关还会为每个进程停止时仍活跃的会话触发一次类型化的 session_end 插件 hook:普通 SIGTERM/SIGINT 停止时 reasonshutdown,预期内重启时是 restart。这段 drain 同样有上限,慢的 session_end handler 阻塞不了进程退出;已经通过替换/重置/删除/压缩完成收尾的会话会被跳过,避免重复触发。

什么情况下不该用内部 hooks

几条边界,都是文档自己划出来的:

  • 需要拦截、改写、阻断的场景不要用。改提示词、拦工具调用、取消外发消息、加有序策略,走插件的 api.on(...) 类型化 hook。内部 hooks 没有阻断/取消语义。
  • 需要常驻资源的不要用。定时器、watcher、socket、长连接客户端一律不能放在内部 hook handler 里,改用插件 service 或 gateway_start / gateway_stop
  • 想靠 message:* 事件跟用户对话的不要用。这些事件推进 event.messages 的内容会被直接丢掉。
  • 遗留的 api.registerHook 别再拿来注册类型化事件名。这个老 SDK 接口只往内部事件系统注册(command:newgateway:startupmessage:received 之类);before_tool_callmessage_receivedsession_start 这些类型化生命周期名字,只由类型化 hook runner 派发,registerHook 不会调到它们。拿它注册类型化名字会收到一条注册警告,指向 api.on(...) 这个公开 API——至少不会静默失败。

还有一块本文没覆盖:插件类型化 hook 的完整参考(before_tool_callbefore_agent_replybefore_install 等在进程内的生命周期 hook)在官方的 Plugin hooks 页面,本文只讲了内部 hooks 这一面。如果你的需求是”在某个动作发生前决定要不要放行”,基本可以直接跳过内部 hooks,从那一页开始读。

延伸阅读


本文依据 OpenClaw 官方仓库(github.com/openclaw/openclawdocs/ 下的官方文档整理,核对日 2026-08-17。 我们没有安装或运行过 OpenClaw,因此不涉及界面外观、操作手感与实测耗时的任何描述; 文中的默认值、命令与配置项均为文档口径,不构成对实际运行结果的保证。 该项目迭代很快,请以仓库最新内容为准。接入即时通讯平台前,请自行确认所在平台的规则与合规要求。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。