OpenClaw hooks 怎么用:事件清单、目录结构与"装了不跑"的三种原因
想让 OpenClaw 在你敲 /new 的时候自动存一份会话快照,或者在消息发出去之后顺手调一个外部接口,官方给的路子是 hooks——跑在网关(Gateway)进程里的小脚本,agent 事件一触发就执行。
麻烦在于 hooks 这个词在 OpenClaw 里指的东西不止一种,而且它默认压根不加载。文档里写得很直白:网关只有在你启用了 hooks,或者至少配置了一个 hook 条目、一个 hook pack、一个额外 hook 目录之后,才会去加载内部 hook。也就是说你把目录建好、handler 写完、重启网关,什么都不会发生——因为网关在启动阶段就跳过了内部 hook 的发现流程。这是最常见的”写完不跑”。
下面按官方文档把这套机制拆开讲:先分清几个长得很像的扩展面,再说事件、目录结构、发现优先级和配置,最后是排查路径和适用边界。
先分清:内部 hooks、webhook、插件类型化 hook 是三件事
OpenClaw 文档特意把这三个摆在一起对比过,因为它们看起来都叫 hook,解决的却不是同一类问题。
| 你想做的事 | 该用的东西 | 文档给的理由 |
|---|---|---|
/new 时存快照、记录 /reset、message: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 可以订阅某个具体的事件键,也可以订阅一个”家族名”(command、session、agent、gateway、message),订阅家族名就能收到该家族下的所有动作。
| 事件 | 触发时机 |
|---|---|
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 是可以诊断出来的。理论上只有插件自己发的自定义事件才会命中非核心名字。
每个事件对象都带 type、action、sessionKey、timestamp、messages 和 context(各事件自己的数据)。context 里具体有什么,文档按事件分别列了,挑几个常用的:
command:new/command:reset:context.sessionEntry、context.previousSessionEntry、context.commandSource、context.senderId、context.workspaceDir、context.cfg。session:auto-reset:多一个context.reason,取值是daily或idle,还有context.transcriptArchived、context.nextSessionId、context.agentId、context.storePath等。message:received:context.from、context.content、context.channelId、context.media,以及context.metadata(渠道方提供的数据,含senderId、senderName、guildId)。要注意context.content不含 agent 侧的富化内容,比如线程历史和链接摘要。message:sent:context.to、context.content、context.success、context.channelId,发送失败时还有context.error。- 压缩事件:
before带messageCount、tokenCount;after再加compactedCount、summaryLength、tokensBefore、tokensAfter。想理解这几个数字对应的是什么过程,可以看会话压缩机制那篇。
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:new 和 command:reset(作为回复路由到原会话),以及 session:compact:before / session:compact:after(作为压缩状态提示发出)。其余全部忽略——包括 command:stop、所有 message:*、agent:bootstrap、session:patch 和 gateway:*。所以你在 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.ts、handler.js、index.ts 或 index.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(要求的 bins、anyBins、env 或 config 路径)、always(在兼容 OS 上跳过 requires.* 检查)、hookKey(配置键覆盖,默认用 hook 名)、homepage(openclaw hooks info 里显示的文档地址)、install(安装方式)。
官方在”最佳实践”里给了四条,都是很实在的工程约束:handler 要快,因为 hook 是在命令处理过程中跑的,重活用 void processInBackground(event) 发射后不管;risky 的操作包 try/catch,不要往外抛异常,否则其他 handler 没法跑;不相关的事件类型尽早 return;事件键尽量写具体的 ["command:new"] 而不是 ["command"],减少开销。
四个来源、有覆盖优先级
hook 从四个地方被发现,顺序本身就是覆盖规则:
- Bundled hooks:跟 OpenClaw 一起发布的内置 hook。
- Plugin hooks:装在插件里的,可以用同名覆盖 bundled。
- Managed hooks:
~/.openclaw/hooks/,用户安装、跨工作区共享,可以覆盖 bundled 和 plugin。hooks.internal.load.extraDirs配的额外目录跟它同一优先级。 - 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 install 和 openclaw hooks update 现在是 openclaw plugins install / openclaw plugins update 的废弃别名。
五个内置 hook,先看看有没有现成的
| Hook | 事件 | 作用 |
|---|---|---|
| session-memory | command:new、command:reset、session:auto-reset | 把会话上下文存到 <workspace>/memory/ |
| bootstrap-extra-files | agent:bootstrap | 按 glob 模式注入额外的 bootstrap 文件 |
| command-logger | command | 把所有命令记到 ~/.openclaw/logs/commands.log |
| compaction-notifier | session:compact:before、session:compact:after | 压缩开始/结束时在聊天里发可见提示 |
| boot-md | gateway: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 存下的对话摘录,如果会话记录稿索引也开着,同一段对话会同时从 memory 和 sessions 两边出现,造成搜索结果重叠和额外的向量化开销。只想要 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"]
}
}
}
}
}
patterns 和 files 是 paths 的别名。路径相对工作区解析,且必须留在工作区内。只有被认可的 bootstrap 基名会被加载:AGENTS.md、SOUL.md、IDENTITY.md、USER.md、BOOTSTRAP.md、MEMORY.md。TOOLS.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.md 和 handler.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 带 reason 和 restartExpectedMs,在关闭开始时触发;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 停止时 reason 是 shutdown,预期内重启时是 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:new、gateway:startup、message:received之类);before_tool_call、message_received、session_start这些类型化生命周期名字,只由类型化 hook runner 派发,registerHook不会调到它们。拿它注册类型化名字会收到一条注册警告,指向api.on(...)这个公开 API——至少不会静默失败。
还有一块本文没覆盖:插件类型化 hook 的完整参考(before_tool_call、before_agent_reply、before_install 等在进程内的生命周期 hook)在官方的 Plugin hooks 页面,本文只讲了内部 hooks 这一面。如果你的需求是”在某个动作发生前决定要不要放行”,基本可以直接跳过内部 hooks,从那一页开始读。
延伸阅读
- 从头读起:OpenClaw 架构总览:网关、Agent、渠道、节点、插件五层各管什么,出问题该往哪查
- 本专题共 40 篇,完整分组目录见专题页
- OpenClaw 常驻指令怎么写:把「每次都要提醒」换成一份带边界的长期授权
- OpenClaw 的 Agent 循环怎么转:从 agent RPC 到落盘的一整条链路
本文依据 OpenClaw 官方仓库(github.com/openclaw/openclaw)docs/ 下的官方文档整理,核对日 2026-08-17。
我们没有安装或运行过 OpenClaw,因此不涉及界面外观、操作手感与实测耗时的任何描述;
文中的默认值、命令与配置项均为文档口径,不构成对实际运行结果的保证。
该项目迭代很快,请以仓库最新内容为准。接入即时通讯平台前,请自行确认所在平台的规则与合规要求。