Hooks 钩子
概述
Hooks 允许你在消息处理的不同阶段插入自定义逻辑,实现消息过滤、内容转换、日志记录等功能。它是 OpenClaw 自动化体系中最灵活的扩展点。
Hook 类型
| Hook | 触发时机 | 典型用途 |
|---|---|---|
| pre-process | 消息到达 Agent 前 | 消息过滤、内容清洗、敏感词拦截 |
| post-process | Agent 响应后 | 格式转换、日志记录、通知转发 |
| on-error | 出错时触发 | 错误告警、自动重试、降级处理 |
配置方式
在 openclaw.json 中配置 Hooks:
{
"hooks": {
"pre-process": [
{
"name": "敏感词过滤",
"script": "scripts/filter.js",
"enabled": true
}
],
"post-process": [
{
"name": "日志记录",
"script": "scripts/logger.js",
"enabled": true
}
],
"on-error": [
{
"name": "错误通知",
"script": "scripts/error-notify.js",
"enabled": true
}
]
}
}使用场景
消息过滤
在消息到达 Agent 前进行预处理:
- 过滤垃圾信息和广告
- 拦截包含敏感信息的消息
- 将语音消息自动转为文字
响应增强
在 Agent 响应后进行后处理:
- 自动翻译为指定语言
- 添加格式化(如 Markdown 渲染)
- 将重要响应同步到其他渠道
安全审计
记录和监控所有交互:
- 记录所有工具调用到审计日志
- 检测异常行为模式
- 在执行危险操作前发送确认请求
二次确认
对危险操作要求用户确认:
{
"security": {
"requireConfirmation": ["delete", "rm", "restart", "shutdown"]
}
}当指令包含这些关键词时,OpenClaw 会先询问「确定要执行吗?回复 yes 确认」。
内置 Hooks
OpenClaw 预置了 4 个开箱即用的 Hook,在安装时可选择启用。初次使用建议先跳过(Skip for now),后续按需在设置菜单中逐个开启。
| Hook | 触发时机 | 功能 | 适用场景 |
|---|---|---|---|
boot-md | 网关启动时 | 执行 BOOT.md 文件中的预设规则 | 需要固定启动流程、强制遵循规则 |
bootstrap-extra-files | Agent 引导阶段 | 将额外工作区规则文件注入上下文 | 拥有完整的 Bootstrap 文件体系 |
command-logger | 每次命令执行时 | 将所有命令事件记录到审计文件 | 需要追踪操作历史、安全审计 |
session-memory | 执行 /new 命令时 | 自动保存当前会话上下文 | 希望在新建会话前保留记忆 |
Bootstrap 文件体系
bootstrap-extra-files 支持注入以下工作区文件:
AGENTS.md— Agent 角色定义SOUL.md— Agent 人格与风格TOOLS.md— 可用工具清单IDENTITY.md— 身份标识USER.md— 用户偏好HEARTBEAT.md— 心跳规则BOOTSTRAP.md— 引导入口
按需启用
- 固定启动规则 → 启用
boot-md - 操作审计需求 → 启用
command-logger - 会话记忆保留 → 启用
session-memory - 不确定 → 先跳过,不影响核心功能,后续随时可在设置中开启
Hook 开发教程
Hook 是 Agent 的自动化 SOP。特定事件发生时,代码直接跑,不经过 LLM 思考。和 Skill 完全不同:
| 维度 | Skill | Hook |
|---|---|---|
| 本质 | 知识注入(SKILL.md) | 代码执行(handler.ts) |
| 执行 | LLM 读后自己决定怎么用 | 事件触发直接跑,不问 LLM |
| 可靠性 | 软——LLM 可能忽略 | 硬——代码一定会执行 |
| 类比 | 给员工操作手册,他可能看可能不看 | 膝跳反射,锤子敲上去腿一定弹 |
Skill 影响 Agent 的想法,Hook 控制系统的行为。一个是建议,一个是铁律。
Hook 的结构
一个文件夹,两个文件:
my-hook/
├── HOOK.md # 元数据:名字、监听什么事件
└── handler.ts # 代码:事件来了干什么放 <工作区>/hooks/ 是项目专用,放 ~/.openclaw/hooks/ 是全局生效。
第一步:写 HOOK.md
HOOK.md 用 YAML front matter 声明元数据:
---
name: my-first-hook # 唯一标识,CLI 管理和配置引用用这个
description: 这个 Hook 干什么的 # 简短描述
metadata:
openclaw:
emoji: "G" # CLI 列表里显示的图标,可选
events: # 监听哪些事件,数组格式
- "session:compact:before"
- "command:new"
requires:
bins: # 运行环境依赖
- "node" # handler.ts 需要 node
---
# 这里写文档(可选)
这个 Hook 的用途说明,给人看的。几个要注意的点:
name用 kebab-case(小写+连字符),是标识符events是数组,一个 Hook 可以监听多个事件requires.bins声明运行依赖,handler.ts 跑在 node 上所以要写 nodeemoji和description都是可选的,不写也能跑
第二步:写 handler.ts
handler 接收 event 对象,三个关键属性:
event.type— 事件大类(command、session、message)event.action— 具体动作(new、compact:before、received)event.messages.push()— 往 Agent 的 context 推消息,这是 Hook 和 Agent 沟通的唯一桥梁
判断当前事件用 type + action 组合。比如 command:new 这个事件:event.type === "command", event.action === "new"。
你不需要会 TypeScript。给 Agent 这个 prompt 就行:
写一个 OpenClaw Hook 的 handler.ts
触发事件:[事件名]
触发时做:[自然语言描述动作]
要求:export default handler,用 event.type/action 判断事件,
用 event.messages.push() 推送消息关键语法点:
- 必须
export default handler,不是 module.exports,不是export { handler } - handler 是 async 函数,异步操作要 await
- 一个 handler 可以处理多个事件
event.messages.push()是 Hook 和 Agent 沟通的唯一桥梁
第三步:放目录 + 验证
mkdir -p <工作区>/hooks/my-first-hook/
openclaw hooks list # 确认系统发现了它
openclaw hooks info my-first-hook # 看配置对不对handler.ts 模板
模板 A:最简——只推一条消息
const handler = async (event) => {
if (event.type !== "command" || event.action !== "new") return;
event.messages.push("新 session 已启动,检查 temp/ 下有没有未完成的计划文件。");
};
export default handler;模板 B:中等——读写文件
import { readFileSync, existsSync } from "fs";
import { join } from "path";
const handler = async (event) => {
if (event.type !== "command" || event.action !== "new") return;
const today = new Date().toISOString().split("T")[0];
const path = join(process.cwd(), "memory", `${today}.md`);
if (existsSync(path)) {
const content = readFileSync(path, "utf-8");
event.messages.push(`工作日志已加载: \n${content}`);
} else {
event.messages.push("今天还没有工作日志,开始新的一天");
}
};
export default handler;模板 C:进阶——条件判断 + 多事件处理
import { readFileSync, writeFileSync, existsSync, readdirSync } from "fs";
import { join } from "path";
const handler = async (event) => {
const tempDir = join(process.cwd(), "temp");
// 处理压缩前事件
if (event.type === "session" && event.action === "compact:before") {
if (!existsSync(tempDir)) return;
const plans = readdirSync(tempDir).filter(f => f.endsWith("-plan.md"));
if (plans.length === 0) return;
const snapshot = {
timestamp: new Date().toISOString(),
plans: plans.map(f => ({
name: f, content: readFileSync(join(tempDir, f), "utf-8")
}))
};
writeFileSync(join(tempDir, "compact-snapshot.json"),
JSON.stringify(snapshot, null, 2));
event.messages.push(`快照已保存,含 ${plans.length} 个计划文件`);
}
// 处理压缩后事件
if (event.type === "session" && event.action === "compact:after") {
const path = join(tempDir, "compact-snapshot.json");
if (!existsSync(path)) return;
const snapshot = JSON.parse(readFileSync(path, "utf-8"));
const list = snapshot.plans.map(p => `- ${p.name}`).join("\n");
event.messages.push(
`context 刚被压缩。压缩前的任务: \n${list}\n` +
`请读取 temp/ 下的计划文件恢复上下文`);
}
};
export default handler;事件速览
11 种事件分 4 类。不用全背,知道有哪些,用到回来查。
命令类(用户操作触发)
| 事件 | 触发时机 | 适合做什么 |
|---|---|---|
command:new | 用户执行 /new 开新 session | 保存上轮会话关键信息、初始化新 session 上下文 |
command:reset | 用户执行 /reset 重置 | 清理临时文件、保存当前状态快照 |
command:stop | 用户执行 /stop 停止 | 收尾工作、保存进度到持久化文件 |
会话类(系统自动触发)
| 事件 | 触发时机 | 适合做什么 |
|---|---|---|
session:compact:before | context 压缩之前 | 赶紧把当前任务状态、关键上下文保存到文件 |
session:compact:after | context 压缩之后 | 把刚保存的关键信息重新注入到压缩后的新 context |
这一对是金矿。context 压缩会让 Agent 变傻——跑着跑着突然忘了自己在干什么。Hook 可以把这个手动操作变成自动的。
引导类(启动时触发)
| 事件 | 触发时机 | 适合做什么 |
|---|---|---|
agent:bootstrap | 工作区引导前 | 注入额外引导文件、设置初始状态 |
gateway:startup | 网关启动后 | 执行启动脚本、加载外部配置 |
消息类(消息流转触发)
| 事件 | 触发时机 | 适合做什么 |
|---|---|---|
message:received | 接收到消息 | 消息预处理、格式转换 |
message:transcribed | 音频转录完成 | 转录文本的后处理、清洗 |
message:preprocessed | 媒体处理完成 | 处理完图片/文件后的后续操作 |
message:sent | 消息发送 | 发送日志、消息存档 |
代理回复类(2026.4.2+)
| 事件 | 触发时机 | 适合做什么 |
|---|---|---|
before_agent_reply | 在调用 LLM 之前,可以直接短路 | 规则命中时合成回复、命中缓存直接返回、只读命令快速应答 |
before_agent_reply 是 2026.4.2 新增的 Hook,允许插件在不触发 LLM 的情况下直接返回合成的 Agent 回复。典型用途:
- 机器人受到触发词如
/help时直接推送帮助文本 - 命中 FAQ / RAG 缓存的问题直接复用历史答案,省掉一次 LLM 调用
- 合规审核未通过时输出固定的拒绝话术
与 pre-process 的区别
pre-process 是在消息进入 Agent 之前修改上下文;before_agent_reply 是在 Agent 准备调用 LLM 时短路掉整个调用链,直接给用户回复。前者改输入,后者改输出。
3 个最值得优先关注的事件:
session:compact:before/after— 解决 Agent 变傻的痛点command:new— 每次开新 session 时自动保存和恢复上下文agent:bootstrap— 动态注入引导信息,比写死更灵活
Hook 发现与管理
三级目录扫描
OpenClaw 启动时自动扫描三个目录找 Hook,优先级递减:
| 优先级 | 位置 | 说明 |
|---|---|---|
| 最高 | <workspace>/hooks/ | 工作区 Hook,当前项目专用 |
| 中等 | ~/.openclaw/hooks/ | 托管 Hook,全局可用 |
| 最低 | 随 OpenClaw 安装自带 | 捆绑 Hook,官方内置 |
优先级的意思是:工作区和托管目录有同名 Hook 时,工作区的优先。
CLI 命令参考
openclaw hooks list # 列出所有已发现的 Hook
openclaw hooks list --eligible # 只显示符合运行条件的(依赖满足的)
openclaw hooks info <hook-name> # 查看某个 Hook 的详细信息
openclaw hooks check # 检查所有 Hook 的合格性
openclaw hooks enable <hook-name> # 启用某个 Hook
openclaw hooks disable <hook-name> # 禁用某个 Hook最常用的是 list 和 info——写完 Hook 放到目录后,先 list 确认系统发现了,再 info 看配置对不对。
list --eligible 排查问题时好用:Hook 出现在 list 但没出现在 list --eligible,说明依赖没满足。
配置文件启用/禁用
在 openclaw.json 里可以精细控制每个 Hook 的开关:
{
"hooks": {
"internal": {
"enabled": true,
"entries": {
"session-memory": { "enabled": true },
"command-logger": { "enabled": false },
"compact-guardian": { "enabled": true }
}
}
}
}顶层 enabled 控制整个 Hook 系统的总开关。entries 里按 Hook 名称单独控制每一个的启用/禁用。
Hook 常见坑
坑 1:事件名拼错
session:compact:before 写成 session:compactBefore——拼错了没报错,默默不触发。解决:复制粘贴事件名,不要手打。
坑 2:目录放错
放在工作区根目录而不是 hooks/ 子目录下。正确路径是 <workspace>/hooks/<hook-name>/。解决:openclaw hooks list 确认系统发现了你的 Hook。
坑 3:导出写法错
必须 export default handler。不是 module.exports,不是 export { handler }。
坑 4:node 没装或没声明
HOOK.md 里要写 requires.bins: ["node"]。没装 node 的话 Hook 静默失败,不报错。
坑 5:异步忘了 await
handler 是 async 函数。忘了 await 异步操作,代码可能没执行完就返回了。