1. 插件目录里到底放了什么?
以 exec-guard / node-guard 为例,/data/openclaw/state/extensions/<插件名>/ 目录下固定两个文件:
- **
openclaw.plugin.json**:插件的”身份证”,声明id、name、description,OpenClaw 靠id字段认出这是哪个插件。 - **
index.ts**:插件真正的逻辑代码,导出一个默认函数。
{
"id": "exec-guard",
"name": "Exec Guard",
"description": "Block dangerous exec commands",
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {}
}
}
configSchema 是空的,意味着这个插件不接受外部可配置参数——规则都硬编码在 index.ts 里。
2. 光写文件不会自动加载
很多人以为把文件夹丢进 extensions/ 目录 OpenClaw 就会自动扫描加载,其实不会。必须再做三件事:
- 把插件
id写进openclaw.json的plugins.allow数组(白名单) - 在
plugins.entries里加一条{"enabled": true} - 执行
openclaw plugins enable <id>显式激活
光靠 enabled: true 不够,必须跑这条命令。
如果插件需要往系统提示词里塞内容(比如 node-guard),还要多加一条权限声明 hooks.allowPromptInjection: true,并把插件目录路径额外登记进 plugins.load.paths,否则加载器根本不会去扫这个目录。
改完配置、enable 完,代码也不会热加载进正在跑的进程,最后还要重启一次 Gateway 才真正生效。
3. 钩子类型不是靠文件名判断的
这是最容易误解的一点:index.ts 这个文件名只是加载器约定的入口文件名(类似 npm 包的 main 字段),跟这个插件是什么类型的钩子完全无关。
真正决定”这是 before_tool_call 钩子还是 before_prompt_build 钩子”的,是代码运行时这一行:
export default function (api) {
api.on("before_tool_call", function (event, ctx) {
// 这个字符串参数决定钩子类型
// ...
});
}
api.on(eventName, handler) 就像 Node.js 里的 EventEmitter.on():OpenClaw 内部维护一张”事件名到监听器列表”的表,插件加载时调用一次你的默认导出函数,你在函数体里调用 api.on() 完成”订阅”。之后每次真的发生对应事件(比如 Agent 要调用 exec 工具),OpenClaw 就把这个事件 emit 出去,依次执行所有订阅了这个事件名的 handler。
一个插件文件里完全可以同时订阅多个事件(api.on("before_tool_call", ...) 又 api.on("before_prompt_build", ...)),互不冲突。
4. 两种钩子,两种权限等级
| 钩子 | 时机 | 能做什么 | 权限要求 |
|---|---|---|---|
before_tool_call |
Agent 即将调用一个工具(如 exec)时 |
返回 {block: true} 可以直接拦截这次调用 |
无额外权限 |
before_prompt_build |
每次组装发给模型的系统提示词时 | 返回 {prependSystemContext: "..."} 可以往提示词开头插内容 |
必须在 openclaw.json 里显式声明 allowPromptInjection: true |
before_tool_call 只是”拦截/放行”的判断,风险较低;before_prompt_build 能直接改模型看到的系统指令,属于更敏感的能力,所以 OpenClaw 单独加了一道权限门禁。
5. 小结:一条命令背后发生了什么
当你执行 openclaw plugins enable exec-guard 并重启 Gateway 后:
- 加载器读
openclaw.plugin.json确认id合法且在白名单里 importindex.ts的默认导出函数,传入api对象并调用一次- 你的代码调用
api.on("before_tool_call", handler),把 handler 挂进事件表 - 以后每次 Gateway Agent 发起
exec调用,OpenClaw 都会先emit("before_tool_call", ...),跑一遍所有订阅者,任何一个返回block: true就直接拦下这次调用