生命周期 Hooks
botmux 可以在关键生命周期事件发生时调用外部命令。默认是异步的:命令失败、超时或不存在只会写日志,不阻塞 botmux 主流程。
另有一类同步前置校验闸(mode: "sync",仅 prompt.submit 事件支持):daemon 会等它跑完,并按它的裁决决定这条消息要不要提交给 CLI。见下文同步前置校验闸。
配置位置
按优先级从高到低:
BOTMUX_HOOKS_JSON环境变量(直接传 JSON 数组)BOTMUX_HOOKS_FILE指定的文件路径- 默认
~/.botmux/data/hooks.json
快速验证:写入本地日志
仓库内置示例脚本,复制即用:
触发任意 hook 事件后即可在日志里看到 JSON payload。examples/hooks/ 还附带 macOS Notification Center(osascript-notify.sh)和 HTTP webhook(http-webhook.sh)示例。
配置字段
outbound.send/outbound.reply不能用来拦截。 它们的发射点在飞书 API 调用成功之后(需先拿到messageId),那一刻消息已经在群里了;加mode:"sync"也只能事后撤回,不是拦截。这两个事件写sync会降级为 async 并告警。
支持事件
Payload 字段
所有 payload 通过 stdin 写入 hook 命令,同时设置环境变量 BOTMUX_HOOK_EVENT。每份 payload 都包含 event、emittedAt;事件上下文可包含 sessionId、chatId、chatType、larkAppId、scope、anchor、title、cliId、workingDir、hasHistory、spawnedAt、lastMessageAt。
不同事件额外携带:
schedule.fired.status 与任务的 lastStatus 使用 ok、error、skipped:ok 表示已交给执行器,不代表模型已完成或消息已送达;error 表示前置检查或提交报错;skipped 表示前置条件未通过、未调用模型。跳过不消耗重复次数,也不删除任务、前置配置或执行日志。已启用的一次性任务跳过后保持启用,等待至少 30 秒后由调度器重新检查;若提前手动运行被跳过,后续自动检查不会早于原定执行时间。周期任务仍按原周期检查。
skipped 是新增状态,旧任务无需迁移;只接受 ok/error 的自定义 Hook 需增加该分支,不能把它当作执行成功。
默认会把 content、message、description、finalOutput、lastScreenContent 截断到 600 字符,并补充 xxxLength / xxxTruncated;只有 redact.fullContentEvents 内的事件透传全文。
同步前置校验闸(prompt.submit)
普通 hook 是「通知」——跑完没人看结果。prompt.submit + mode: "sync" 是「裁决」:daemon 等它、读它、按它放行或拒绝。用于在消息进入 CLI 前做一层自定义权限校验(内部权限服务、工作时间限制、高危指令拦截等)。
仓库内置可直接改的示例:examples/hooks/prompt-gate.sh。
触发时机
闸跑在「prompt 还没送进 CLI 之前」——不是「已经输进输入框、按 Enter 之前」。
后一种时机在 botmux 里不存在:写文本与按 Enter 是同一次适配器调用中的原子动作(writeInput 逐行打字、末尾那个 Enter 即提交),中间没有可插入的停顿。
闸执行于 daemon 进程,此时 CLI 子进程尚未拿到这一轮输入(新话题下 CLI 甚至还没 fork)。被拒的消息,CLI 完全不知道它存在过。
也可以只当通知用(async)
这个事件不强制你写 mode:"sync"。不写 mode(或显式写 async)时它就是一个普通通知 hook:daemon 不等它、它的结果不影响放行,适合做审计流水、统计、告警。
两条可以同时存在:上面那条只记录,下面那条真拦截。几条约定:
- 通知在裁决之前投递,所以即使这条消息最终被拒,通知 hook 仍会收到它——「谁的消息被拦了」正是审计想知道的事实。
- 通知拿到的是截断后的正文(默认 600 字符,与其它事件一致)。只有做裁决的
synchook 才豁免截断——通知型 hook 不因为订阅了这个事件就拿到完整正文。 - 通知型 hook 的退出码和 stdout 一律不参与裁决。
怎么表达裁决
两种写法,stdout 的 JSON 优先于退出码:
stdout 必须是整段 JSON 对象才会被当作裁决。打印一行普通日志不会被误判成裁决——那种情况回退到看退出码。
边界与保证
- 只能收紧,不能放宽:内置权限模型(
allowedUsers/grant/ oncall / 额度)先跑,全部通过后才会问这个 hook。hook 说allow不会让内置闸拒掉的人进来。 - 拒绝不扣额度:闸排在扣费之前,被拒的消息不消耗用户的消息额度。
- 拒绝会明确告诉用户(附
reason),不静默丢弃——有权限却消息凭空消失是最难排查的形态。 - 多个 sync hook 是 AND:任一
deny即拒绝,第一个deny之后的 hook 不再执行。 - hook 坏了不等于拒绝:超时、找不到命令、崩溃都走
onError,默认allow——校验器挂掉不该让整个 bot 变砖头。要反过来就显式写onError: "deny"。 - ⚠️
timeoutMs: 0不是「不限时」,而是「立即超时」:闸会瞬间走onError(默认allow)=这道闸等于没装。加载时会打一条 warn 提醒。要不限时是做不到的(闸在收信主路上),请写一个真实预算如3000。 - 延迟直接加在收信路径上:
timeoutMs建议设小(1-3s)。bot 级并发不会因此卡死整个 daemon,但慢闸会拖住两类东西:①同一话题的续聊持有顺序锁 ⟹ 该话题后续消息排队;②闸运行在该 Bot 的 admission lease 内 ⟹ 该 Bot 的 Dashboard 运维操作(关闭/清理会话、改 Agent 配置等)在闸返回前可能超时。别指望用大超时兜住一个慢服务。没配 sync hook 时零开销,不会给每条消息加 spawn。 - 消息监听器(message listener)命中的第三方内容也会过闸:那类内容来自告警 bot 等外部来源、同样会进 CLI,正是最该校验的。该路径本来就不扣额度,被拒时只记日志、不回消息(没有可回复的真人发送者)。
- 闸拿到的是完整正文,不受 600 字符截断影响:截断是为通知类 hook 设计的,而闸的判断依据就是内容本身——截断会让它对超长输入结构性失明(把恶意内容垫到 600 字符之后即可绕过)。 ⚠️ 隐私含义:配了 sync 闸就等于把完整消息正文交给那个命令。异步 hook 仍按原规则截断,未受影响。
- 闸看得到附件的元信息,但看不到附件内容:
attachments字段给出本轮的[{type,name}](如[{"type":"file","name":"prod.env"}]),足以写「禁止上传 .env」「只许图片」这类策略;但闸跑在附件下载之前(下载必须排在授权之后,否则未授权者也能让 bot 去拉文件),所以无法按文件内容判断。 - 覆盖范围:只管「人/外部消息进 CLI」这条入口。新话题、话题续聊、斜杠命令冷启动、会话群出生轮、消息监听器命中——都过闸。定时任务自动跑出来的 prompt 不过闸(那是运维自己预先授权的自动化,不是外部输入)。v3 saved workflow 会过闸,但不传正文——
senderOpenId等发送者级规则仍然生效,内容级规则对它失明。别把它当成「所有进 CLI 的文本都查过了」。 - 同一条 hook 配置只会跑一次:作为闸执行后,不会再作为异步通知重复触发。
快速验证
在飞书里发一条消息:应当收到「本条消息被前置校验拦截」的回复,且 /tmp/gate-payload.json 里能看到本轮 payload。验证完记得清空 hooks.json。
实践:用 session.start hook 自动更新 Skills
botmux 原生集成了 agentbuddy 作为 skill 来源(botmux skills install <agentbuddy命令> 安装,botmux skills update <name> 更新)。配合 session.start hook,可以在每次新会话启动时自动检查并更新已安装的 skills,等效于 Relay / Claude Code settings.json 中的 SessionStart Hook。
更新单个 skill
更新全部已安装 skills
botmux skills update 只接受单个 skill 名称,不支持 * 或正则。更新全部需要脚本循环:
直接调用 agentbuddy CLI 更新用户全局 Skills
如果想直接运行 npx agentbuddy update(更新用户全局 skills,而非 botmux 管理的 skills),需要注意 botmux hook 的执行环境限制:shell: false(不支持重定向、管道)、环境变量被清洗(只保留 PATH/HOME/TMPDIR/SHELL/USER 等基础项)。建议写成包装脚本:
注意事项
- 超时:默认
timeoutMs为 5000ms,agentbuddy update 涉及网络请求通常需要更久,必须显式加大(建议 60s+)。超时后 botmux 会先SIGTERM再SIGKILL整个进程组。 - fire-and-forget:hook 是异步执行,不会阻塞会话启动;skill 更新完成后需新会话才生效。
- filter 过滤:可用
filter限定只对特定chatId或senderOpenId生效,避免所有会话都跑更新。 - 推荐方式:优先使用
botmux skills update(方式一),它经过 botmux 的 telemetry 清理(clearAgentbuddyTelemetry),更新的是 botmux 注入的 skill 版本,与 botmux skill 生命周期一致。
写自己的 hook
hook 命令可以是任意 executable:bash / Python / Node / Go 二进制、公司内部 CLI、HTTP 转发器都行。命令 exit 0 视为成功;非 0 / 超时 / 找不到命令只写 botmux 日志,不会影响收发消息、定时任务或 session 生命周期。
