钩子
钩子是 Mework 在一轮对话的固定时刻运行的命令:你发出消息时、每次工具调用前后、模型准备结束时。用它可以检查、拦截或改写将要发生的事,也可以给模型补充上下文。格式沿用 Claude Code 的 hooks 块,从 Claude Code 拷来的配置不用改就能用。
hooks.json 放在哪里
| 层级 | 文件 | 谁会用到 |
|---|---|---|
| 全局 | ~/.mework/hooks.json(Windows:%USERPROFILE%\.mework\hooks.json) |
所有对话 |
| 工作区 | <工作区>/.mework/hooks.json |
包含这个工作区的对话 |
对话可以从全局钩子和它所有工作区的钩子里挑选。全局钩子在本机运行。工作区在 WSL 或 SSH 机器上时,Mework 在那台机器上读取工作区的 hooks.json,其中的钩子也在那台机器上运行。对话在某个工作区的隔离工作树里工作时,这个工作区的 hooks.json 仍从它自己的文件夹读取。临时项目的临时文件夹没有 .mework;那里的对话能用全局钩子,以及它附加的工作区的钩子。旧的 .naiword/hooks.json 只在同一层级没有 .mework/hooks.json 时才会被读取。
文件格式
<工作区>/.mework/hooks.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "^(bash|zsh|sh|powershell)$",
"hooks": [
{
"type": "command",
"name": "No force push",
"command": "python3 .mework/hooks/no_force_push.py",
"commandWindows": "python .mework/hooks/no_force_push.py",
"timeout": 10
}
]
}
]
}
}<工作区>/.mework/hooks/no_force_push.py:
import json, sys
call = json.load(sys.stdin)
if "push --force" in call["tool_input"].get("command", ""):
print("Force pushes are not allowed in this project.", file=sys.stderr)
sys.exit(2)hooks 下每个事件对应一组列表;每组有一个可选的 matcher 和一组处理程序。文件不存在、不是合法 JSON、顶层没有 hooks 对象时,就没有任何钩子。文件开头带 UTF-8 字节顺序标记(BOM)没有关系。
事件
事件名区分大小写;其他名字会被跳过。
| 事件 | 什么时候触发 | matcher 匹配的对象 |
|---|---|---|
SessionStart |
Mework 启动后对话的第一轮,所以重启后会再触发一次 | startup |
UserPromptSubmit |
每一轮,在第一次请求模型之前 | (忽略) |
PreToolUse |
每次工具调用之前,先于审批判断 | 工具名 |
PermissionRequest |
调用将要弹出审批卡,或者 PreToolUse 钩子要求弹卡时 |
工具名 |
PostToolUse |
工具运行之后;运行前就被拒绝的调用不触发 | 工具名 |
Stop |
模型不再调用工具、准备结束这一轮时 | (忽略) |
InstructionsLoaded |
加载 MEWORK.md、规则文件或导入的文件时;退出码和输出都不改变任何事 |
加载原因 |
子代理和工作流步骤会运行 PreToolUse、PermissionRequest、PostToolUse 和 InstructionsLoaded 钩子。SessionStart、UserPromptSubmit 和 Stop 只在对话自己的回合里运行。
matcher
- matcher 是正则表达式,而且不锚定:
edit也会匹配edit_global_memory。只想匹配一个工具时写^edit$。 - 不写、写空字符串或写
"*"都匹配全部。正则写错时,整组被丢弃。 - Claude Code 的工具名(
Bash、Read、Write|Edit等)会匹配 Mework 里做同样事情的工具;Mework 自己的名字(bash、write、edit等,见内置工具)也可以用。 - 要匹配某个 MCP 服务器的全部工具,用
^mcp__<服务器名>_。
处理程序的键
| 键 | 作用 | 默认值 |
|---|---|---|
type |
必须是 "command"。其他类型或不写的处理程序会被跳过。 |
— |
command |
必填。要运行的命令。在本机时,macOS 上用 bash -lc <command>;Windows 上用 pwsh,没有时用 Windows PowerShell,输出固定为 UTF-8。在 WSL 或 SSH 机器上,用哪种 shell 由那台机器的系统决定。 |
— |
commandWindows |
钩子在 Windows 机器上运行时替代 command。 |
— |
name |
列表和时间线卡片上显示的名字。 | statusMessage,再没有就是 <事件> #<n> |
statusMessage |
钩子运行时卡片上显示的文字。 | 「正在执行」 |
timeout |
秒数,1–600。超出范围时这个处理程序被移除。 | 30 |
async |
只有 InstructionsLoaded 允许设为 true;其他事件上设为 true 会移除这个处理程序,asyncRewake: true 也一样。 |
— |
为对话选择钩子
- 打开 更多选项 → 对话设置 → 钩子(见对话设置)。
- 勾选这个对话要运行的处理程序。每行是一个处理程序,说明里写着它的事件和 matcher,例如「工具执行前 · matcher ^bash$」。
保存 hooks.json 后,列表很快会自动刷新;按 重新扫描 可以立即刷新,运行时总是按磁盘上的文件为准。列表先是 全局 一节,再按工作区各分一节(见对话设置)。每节标题上的文件夹按钮(打开全局配置目录 或 打开 {path} 的配置目录)会显示这一层的 hooks.json,需要时先创建 .mework 文件夹;对 WSL 或 SSH 工作区,它会在文件面板里打开那台机器上的文件夹。内置的 mework 预设不选任何钩子。
已选的钩子会列在系统提示词里,所以模型的提示缓存未过期时,勾选或取消勾选钩子会标成橙色,并在生效前询问一次。
注意 选择总是指向你勾选的那个处理程序。在
hooks.json里插入、删除或调整处理程序的顺序后,处理程序挪了位置或被删掉的选择会显示为 悬空,在取消勾选之前每次运行都会失败;取消后再勾选处理程序的新行。
每轮开始前的确认
在 手动 和 允许编辑 两个安全级别下,选了钩子的对话每轮开始前都会弹出对话框「确认本轮生命周期钩子」,列出每个钩子的事件、名称、matcher、命令,以及它在哪个文件夹运行(WSL 或 SSH 工作区的钩子还写明机器)。自己选了钩子的代理角色,它的钩子也列在里面,标着角色名;子代理只运行对话框里列过的钩子。点 允许本轮 继续,点 取消 结束这一轮。列出的内容超过 12,000 个字符时,这一轮会被拒绝,请少选一些钩子。完全访问 下不弹出。
钩子怎样运行
- 在哪里: 工作区的钩子在这个工作区所在的机器上运行,以工作区文件夹(或对话在这个工作区的工作树)为工作目录。全局钩子在本机运行,以工作区 1 的文件夹为工作目录;但工作区 1 在 WSL 或 SSH 机器上、或者是临时项目的文件夹时,全局钩子改在 Mework 数据目录下的一个临时文件夹里运行。钩子输入里的
cwd写明钩子在哪个文件夹里运行。 - 环境: 在本机运行的钩子用 Mework 的完整环境,在 WSL 或 SSH 机器上运行的钩子用那台机器自己的环境,都再加上钩子所属工作区的 环境变量(见工作区),另有值为事件名的
MEWORK_HOOK_EVENT,以及值为钩子所在文件夹的CLAUDE_PROJECT_DIR。 - 时间: 同一事件匹配到的处理程序并行运行,各自受
timeout限制。同一轮里所有钩子共用 5 分钟的总预算,用完后,后续钩子失败。
哪些钩子会看到一次调用
全局钩子什么都看得到。工作区的钩子在以下时候运行:
- 在这个工作区里工作的工具调用:
workspace参数指向它的工具,例如read、write、edit、grep、find、ls、lsp、各个 shell 和preview_start(调用没写workspace时算工作区 1);指向它的项目记忆工具;这个工作区自己的 MCP 服务器;以及在它里面运行的全局"workspace": true服务器实例(见 MCP 服务器); - 对话自己的事件:
SessionStart、UserPromptSubmit、Stop,以及不在某一个工作区里工作的工具调用,例如web_search、web_fetch、agent_spawn、只用 id 指定的预览服务器,以及没有"workspace": true的全局 MCP 服务器。
工作区的钩子从不为别的工作区的工具调用运行。InstructionsLoaded 只送到全局钩子和工作区 1 的钩子,因为指令文件属于工作区 1。
stdin 输入
每个钩子都会从 stdin 收到一个 UTF-8 编码的 JSON 对象,包含 session_id(对话)、transcript_path(总是 null)、cwd、hook_event_name、model、permission_mode 和 turn_id。permission_mode 在手动下是 default,允许编辑下是 acceptEdits,完全访问下是 bypassPermissions。各事件另外附带:
| 事件 | 附加字段 |
|---|---|
SessionStart |
source:总是 "startup" |
UserPromptSubmit |
prompt:你最新一条消息的文字 |
PreToolUse |
tool_name、tool_use_id、tool_input |
PermissionRequest |
tool_name、tool_input、permission_suggestions(总是空的) |
PostToolUse |
tool_name、tool_use_id、tool_input(改写之后的)、tool_response |
Stop |
stop_hook_active、last_assistant_message |
InstructionsLoaded |
file_path、memory_type、load_reason,有时还有 globs、trigger_file_path、parent_file_path;没有 transcript_path |
tool_input 和 tool_response 用的是 Claude Code 脚本读取的字段名,例如 file_path、old_string、new_string 和 command。
钩子怎样回应
退出码
- 2:拦截,原因取 stderr。
PreToolUse和PermissionRequest拒绝这次调用;UserPromptSubmit和SessionStart在请求模型之前结束这一轮;PostToolUse拒收结果;Stop让模型继续干活。 - 其他非零退出码算作钩子失败,不做任何决定,这一轮照常进行。
- 0:读取 stdout。为空则什么都不做;是 JSON 对象则按下表处理;是纯文本时,对
SessionStart和UserPromptSubmit作为补充上下文,对Stop算作错误,对其他事件忽略。
JSON 输出
| 字段 | 效果 |
|---|---|
continue: false |
结束这一轮,原因取 reason,没有时取 stopReason。 |
decision: "block" 加 reason |
拦截,效果同退出码 2。用在 Stop 上时让模型继续:reason 作为一条新的用户消息发出,连续最多 3 次。 |
systemMessage |
显示在钩子的时间线卡片上,模型看不到。 |
hookSpecificOutput.hookEventName |
必须等于当前事件名,否则整个 hookSpecificOutput 都被忽略。 |
hookSpecificOutput.additionalContext |
给模型的文字,在当前这批工具结果之后以 送达了钩子补充的上下文 卡片送达;如果这一轮先结束了,就在下一轮送达。 |
hookSpecificOutput.permissionDecision(PreToolUse) |
allow:这次调用不弹审批卡。ask:即使在完全访问下、或之前选过 总是允许,也会弹卡,卡上写明是哪个钩子要求确认。deny:拒绝这次调用,原因取 permissionDecisionReason。 |
hookSpecificOutput.updatedInput(PreToolUse) |
配合 allow 或 ask,在审批判断之前替换调用参数。 |
hookSpecificOutput.decision(PermissionRequest) |
{"behavior": "allow", "updatedInput": {…}} 只批准这一次调用。{"behavior": "deny", "message": "…", "interrupt": true} 拒绝调用,interrupt 还会结束这一轮。 |
顶层的 decision: "approve" 或 "block"(PreToolUse) |
旧写法,只在没有 hookSpecificOutput 时生效。 |
多个钩子回应同一次调用时:任何一个拦截或拒绝都会生效;任何一个 ask 都会弹卡;只有所有返回 allow 或 ask 的钩子给出相同的 updatedInput 时,allow 才算数。
PostToolUse 拦截不会撤销调用:模型会被告知工具已经执行完毕,效果仍然保留。
钩子跳不过的确认
以下确认任何钩子都免除不了:每次都必须询问的 MCP 工具(见 MCP 服务器)、危险的递归删除、写入全局记忆,以及在你已登录的浏览器页面上操作。
查看运行记录
除 InstructionsLoaded 外,每次钩子运行都会在时间线上留下一张卡片,展开可以看到 stdout、stderr、systemMessage 和决定;给模型补充了上下文的钩子带有 已加入模型上下文 标记。这些卡片只留在你的电脑上。更多选项 → 历史记录 也会把每次钩子运行与请求、工具调用列在一起。