| 1 | # 桌面端 Hooks 使用说明 |
| 2 | |
| 3 | <a href="../README.zh-CN.md">README</a> |
| 4 | · |
| 5 | <a href="./GUIDE.zh-CN.md">使用指南</a> |
| 6 | · |
| 7 | <a href="./SPEC.md">规格</a> |
| 8 | |
| 9 | Hooks 让 Reasonix 在会话、用户输入、工具调用、模型返回、压缩上下文等节点执行本地 shell 命令。桌面端在“设置 -> Hooks”里提供图形化编辑入口,本质上读写同一份 `settings.json`。 |
| 10 | |
| 11 | > Hook 命令会在本机执行 shell。全局和项目 hooks 都会从各自配置位置自动加载。 |
| 12 | |
| 13 | ## 快速开始 |
| 14 | |
| 15 | 1. 打开桌面端“设置 -> Hooks”。 |
| 16 | 2. 选择范围: |
| 17 | - “全局”:保存到 `<Reasonix home>/settings.json`,始终加载;Windows 默认是 `%APPDATA%\reasonix\settings.json`,macOS/Linux 默认是 `~/.reasonix/settings.json`。 |
| 18 | - “项目”:保存到当前工作区的 `.reasonix/settings.json`,打开项目时自动加载。 |
| 19 | 3. 在 JSON 配置框里编辑 `hooks`。 |
| 20 | 4. 保存后,重启桌面端,让新配置进入会话。`/new` 只开启新对话,不会重新读取 hooks 配置。 |
| 21 | |
| 22 | 示例: |
| 23 | |
| 24 | ```json |
| 25 | { |
| 26 | "hooks": { |
| 27 | "PreToolUse": [ |
| 28 | { |
| 29 | "match": "bash", |
| 30 | "command": "node .reasonix/hooks/check-bash.js", |
| 31 | "description": "Block dangerous shell commands", |
| 32 | "timeout": 5000 |
| 33 | } |
| 34 | ], |
| 35 | "Stop": [ |
| 36 | { |
| 37 | "command": "echo Reasonix turn finished" |
| 38 | } |
| 39 | ] |
| 40 | } |
| 41 | } |
| 42 | ``` |
| 43 | |
| 44 | ## 配置文件位置 |
| 45 | |
| 46 | | 范围 | 文件 | 加载方式 | 加载顺序 | |
| 47 | | --- | --- | --- | --- | |
| 48 | | 全局 | `<Reasonix home>/settings.json` | 自动 | 项目 hooks 之后 | |
| 49 | | 项目 | `<workspace>/.reasonix/settings.json` | 自动 | 全局 hooks 之前 | |
| 50 | |
| 51 | 同一个事件下,项目 hooks 先运行,全局 hooks 后运行;同一范围内按数组顺序运行。阻塞型事件遇到第一个阻塞 hook 后,会停止继续执行后面的 hook。 |
| 52 | |
| 53 | ## 配置 JSON 格式 |
| 54 | |
| 55 | 推荐写法是一个带 `hooks` 字段的对象: |
| 56 | |
| 57 | ```json |
| 58 | { |
| 59 | "hooks": { |
| 60 | "PreToolUse": [ |
| 61 | { "match": "bash", "command": "node .reasonix/hooks/pre-tool.js" } |
| 62 | ], |
| 63 | "UserPromptSubmit": [ |
| 64 | { "command": "node ~/.reasonix/hooks/check-prompt.js" } |
| 65 | ], |
| 66 | "Stop": [ |
| 67 | { "command": "osascript -e 'display notification \"Turn done\" with title \"Reasonix\"'" } |
| 68 | ] |
| 69 | } |
| 70 | } |
| 71 | ``` |
| 72 | |
| 73 | 桌面端 JSON 编辑器也接受两种便捷输入,保存前会格式化回 `{"hooks": ...}`: |
| 74 | |
| 75 | ```json |
| 76 | { |
| 77 | "PreToolUse": [ |
| 78 | { "match": "bash", "command": "node .reasonix/hooks/pre-tool.js" } |
| 79 | ], |
| 80 | "Stop": [ |
| 81 | { "command": "echo done" } |
| 82 | ] |
| 83 | } |
| 84 | ``` |
| 85 | |
| 86 | ```json |
| 87 | [ |
| 88 | { "event": "PreToolUse", "match": "bash", "command": "node .reasonix/hooks/pre-tool.js" }, |
| 89 | { "event": "Stop", "command": "echo done" } |
| 90 | ] |
| 91 | ``` |
| 92 | |
| 93 | 每个 hook 对象支持这些字段: |
| 94 | |
| 95 | | 字段 | 类型 | 说明 | |
| 96 | | --- | --- | --- | |
| 97 | | `command` | string | 必填。通过平台 shell 执行的命令。空字符串会被忽略。 | |
| 98 | | `match` | string | 仅 `PreToolUse`、`PostToolUse` 使用。锚定正则,空字符串或 `*` 表示匹配所有工具。 | |
| 99 | | `description` | string | 可选。显示在 hooks 列表或设置页里的说明。 | |
| 100 | | `timeout` | number | 可选。毫秒数。未设置时,阻塞型事件默认 5000ms,其它事件默认 30000ms。 | |
| 101 | | `cwd` | string | 可选。覆盖 hook 命令工作目录。默认使用当前会话的 `cwd`。 | |
| 102 | |
| 103 | `match` 是锚定正则:`"file"` 不会匹配 `read_file`,需要写成 `".*file"`。正则非法时该 hook 不会触发。 |
| 104 | |
| 105 | `command` 默认通过平台 shell 执行:macOS/Linux 使用 `sh -c`,Windows 使用 |
| 106 | `cmd /c`。如果 Windows hook 自己显式写了裸命令 `sh -c` 或 `bash -c`,Reasonix |
| 107 | 会查找 Git for Windows 自带的 Bash 并直接使用它;带目录的显式解释器路径保持不变。 |
| 108 | 通过 `[tools.shell]` 配置的自定义 Bash 路径同样会被 Hook 复用;找不到 Git Bash 时, |
| 109 | 插件 Doctor 和能力诊断会提前显示可操作的依赖提示。Hook stdout/stderr 中的 Windows 旧代码页 |
| 110 | 文本会转换为 UTF-8,避免中文错误信息显示成乱码。stdin 是 Reasonix 写入的一行 JSON, |
| 111 | 见下面的 payload 表。 |
| 112 | |
| 113 | ## 配置里的事件 key |
| 114 | |
| 115 | 下面这些字符串就是 `hooks` 对象里的事件 key,也是在数组写法中 `event` 字段的取值: |
| 116 | |
| 117 | | 事件 key | 触发时机 | 是否可阻塞 | stdout 特殊作用 | |
| 118 | | --- | --- | --- | --- | |
| 119 | | `PreToolUse` | 工具权限已通过、工具真正执行前 | 是 | 无特殊作用 | |
| 120 | | `PostToolUse` | 工具执行后,不论成功或失败 | 否 | 无特殊作用 | |
| 121 | | `UserPromptSubmit` | 用户输入提交后、本轮模型调用前 | 是 | 无特殊作用 | |
| 122 | | `Stop` | 一轮对话结束后 | 否 | 无特殊作用 | |
| 123 | | `PostLLMCall` | 模型流式返回完成后,reasoning 入库前 | 否 | exit 0 且 stdout 非空时,用 stdout 替换展示的 reasoning | |
| 124 | | `SessionStart` | 会话第一次变为活跃,或 `/new`、清空后新会话开始 | 否 | stdout 会作为下一轮模型上下文注入 | |
| 125 | | `SessionEnd` | 会话关闭、切换、`/new`、清空或控制器释放时 | 否 | 无特殊作用 | |
| 126 | | `SubagentStop` | 前台 `task` 子代理完成后 | 否 | 无特殊作用 | |
| 127 | | `Notification` | 需要用户注意时,例如等待工具审批 | 否 | 无特殊作用 | |
| 128 | | `PreCompact` | 上下文压缩开始前 | 否 | stdout 会追加为压缩摘要的额外指导 | |
| 129 | |
| 130 | 只有 `PreToolUse` 和 `UserPromptSubmit` 是阻塞型事件。阻塞型事件中,命令 `exit 2` 或超时会阻断后续执行。 |
| 131 | |
| 132 | ## Hook 命令收到的 payload |
| 133 | |
| 134 | Reasonix 会把一行 JSON 写入 hook 命令的 stdin。所有 payload 都至少有: |
| 135 | |
| 136 | | key | 类型 | 说明 | |
| 137 | | --- | --- | --- | |
| 138 | | `event` | string | 当前事件 key。 | |
| 139 | | `cwd` | string | 当前会话工作目录,也就是 hook 默认执行目录。 | |
| 140 | |
| 141 | 其它 key 按事件出现;空值会被省略。 |
| 142 | |
| 143 | | 事件 key | 额外 payload key | 示例 | |
| 144 | | --- | --- | --- | |
| 145 | | `PreToolUse` | `toolName`, `toolArgs` | `{"event":"PreToolUse","cwd":"/repo","toolName":"bash","toolArgs":{"command":"go test ./..."}}` | |
| 146 | | `PostToolUse` | `toolName`, `toolArgs`, `toolResult` | `{"event":"PostToolUse","cwd":"/repo","toolName":"bash","toolArgs":{"command":"go test ./..."},"toolResult":"ok"}` | |
| 147 | | `UserPromptSubmit` | `prompt`, `turn` | `{"event":"UserPromptSubmit","cwd":"/repo","prompt":"修复测试","turn":1}` | |
| 148 | | `Stop` | `lastAssistantText`, `turn` | `{"event":"Stop","cwd":"/repo","lastAssistantText":"已修复","turn":1}` | |
| 149 | | `PostLLMCall` | `reasoning`, `turn` | `{"event":"PostLLMCall","cwd":"/repo","reasoning":"raw reasoning","turn":1}` | |
| 150 | | `SessionStart` | 无 | `{"event":"SessionStart","cwd":"/repo"}` | |
| 151 | | `SessionEnd` | 无 | `{"event":"SessionEnd","cwd":"/repo"}` | |
| 152 | | `SubagentStop` | `lastAssistantText` | `{"event":"SubagentStop","cwd":"/repo","lastAssistantText":"子代理结论"}` | |
| 153 | | `Notification` | `message` | `{"event":"Notification","cwd":"/repo","message":"approval needed: bash go test ./..."}` | |
| 154 | | `PreCompact` | `trigger` | `{"event":"PreCompact","cwd":"/repo","trigger":"manual"}` | |
| 155 | |
| 156 | `toolArgs` 是工具参数的原始 JSON。比如 `bash` 通常会带 `{"command":"..."}`,其它工具会按自己的 schema 传入。`Notification.message` 会做必要的隐私收敛,例如记忆审批只包含工具名,不把记忆正文发给外部通知 hook。 |
| 157 | |
| 158 | ## 退出码和输出 |
| 159 | |
| 160 | | 结果 | 阻塞型事件 | 非阻塞型事件 | |
| 161 | | --- | --- | --- | |
| 162 | | exit 0 | 通过 | 通过 | |
| 163 | | exit 2 | 阻塞 | 警告 | |
| 164 | | 其它非零退出码 | 警告,不阻塞 | 警告 | |
| 165 | | 超时 | 阻塞 | 警告 | |
| 166 | | 命令启动失败 | 错误提示,不阻塞 | 错误提示 | |
| 167 | |
| 168 | stdout 和 stderr 会被捕获、去掉首尾空白,并限制单路输出最多 256KB。非通过结果会显示为 warning,优先展示 stderr,其次展示 stdout。 |
| 169 | |
| 170 | 特殊 stdout 行为: |
| 171 | |
| 172 | - `PostLLMCall`:exit 0 且 stdout 非空时,stdout 会替换用户看到的 reasoning。若 provider 的 reasoning 带签名,Reasonix 会保留原始 signed reasoning 用于后续请求,同时仍展示 hook 转换后的文本。 |
| 173 | - `SessionStart`:exit 0 且 stdout 非空时,stdout 会作为一次性 `<hook-context event="SessionStart">` 注入下一轮真实用户输入。纯文本 stdout 会原样作为上下文;也可以输出 Claude Code / Codex 兼容 JSON: |
| 174 | |
| 175 | ```json |
| 176 | { |
| 177 | "hookSpecificOutput": { |
| 178 | "hookEventName": "SessionStart", |
| 179 | "additionalContext": "Load the workspace conventions before editing." |
| 180 | } |
| 181 | } |
| 182 | ``` |
| 183 | |
| 184 | `hookEventName` 必须与当前事件一致。该上下文不会写入 system prompt、工具 schema 或项目记忆;它只影响下一轮模型请求。单个 hook 上下文最多保留约 10000 字符,总量最多约 20000 字符,超出会截断并标记。 |
| 185 | - `PreCompact`:所有非空 stdout 会按换行拼接,作为本次压缩摘要的额外指导。 |
| 186 | - 其它事件:stdout 只在非通过结果中作为提示文本使用,不会自动进入模型上下文。 |
| 187 | |
| 188 | ## 示例:SessionStart 注入启动上下文 |
| 189 | |
| 190 | ```json |
| 191 | { |
| 192 | "hooks": { |
| 193 | "SessionStart": [ |
| 194 | { |
| 195 | "command": "printf '%s\\n' '{\"hookSpecificOutput\":{\"hookEventName\":\"SessionStart\",\"additionalContext\":\"Before coding, check the available skills and follow matching workflows.\"}}'" |
| 196 | } |
| 197 | ] |
| 198 | } |
| 199 | } |
| 200 | ``` |
| 201 | |
| 202 | 这适合把插件或工作流的 bootstrap 说明带入会话。比如 Superpowers 不需要内置到 Reasonix;可以让它自己的 `hooks/session-start-codex` 在 `SessionStart` 输出 `additionalContext`,或让插件根目录 `CLAUDE.md` 被插件包兼容层直接作为 `SessionStart` 上下文读取,Reasonix 会在下一轮把这段说明注入模型上下文。插件包兼容层也会读取 `.claude/settings.json` 里的 command hooks,并按同名事件映射到 Reasonix hooks。Reasonix 默认允许 `max_subagent_depth = 2`,因此 Superpowers 的父会话或第一层 workflow subagent 可以再派发 reviewer/implementer subagent;第二层不会继续获得递归委派工具。若要恢复旧的单层边界,设 `agent.max_subagent_depth = 1`。这会改变子代理可见工具面,可能影响子代理请求的 prompt cache,但不会把 Superpowers 写进 Reasonix 的稳定 system prompt。 |
| 203 | |
| 204 | ## 示例:阻止危险 bash 命令 |
| 205 | |
| 206 | `.reasonix/settings.json`: |
| 207 | |
| 208 | ```json |
| 209 | { |
| 210 | "hooks": { |
| 211 | "PreToolUse": [ |
| 212 | { |
| 213 | "match": "bash", |
| 214 | "command": "node .reasonix/hooks/block-dangerous-bash.js", |
| 215 | "description": "Block risky bash commands", |
| 216 | "timeout": 3000 |
| 217 | } |
| 218 | ] |
| 219 | } |
| 220 | } |
| 221 | ``` |
| 222 | |
| 223 | `.reasonix/hooks/block-dangerous-bash.js`: |
| 224 | |
| 225 | ```js |
| 226 | const fs = require("fs"); |
| 227 | |
| 228 | const payload = JSON.parse(fs.readFileSync(0, "utf8")); |
| 229 | const command = payload.toolArgs?.command || ""; |
| 230 | |
| 231 | if (/\brm\s+-rf\b/.test(command) || /\bgit\s+push\b/.test(command)) { |
| 232 | console.error(`blocked dangerous command: ${command}`); |
| 233 | process.exit(2); |
| 234 | } |
| 235 | ``` |
| 236 | |
| 237 | `exit 2` 会让 `PreToolUse` 阻断该工具调用,并把错误信息反馈给界面和模型。 |
| 238 | |
| 239 | ## 示例:压缩前追加摘要重点 |
| 240 | |
| 241 | ```json |
| 242 | { |
| 243 | "hooks": { |
| 244 | "PreCompact": [ |
| 245 | { |
| 246 | "command": "printf '%s\n' 'Keep exact user decisions, file paths, and unresolved TODOs.'" |
| 247 | } |
| 248 | ] |
| 249 | } |
| 250 | } |
| 251 | ``` |
| 252 | |
| 253 | 当自动压缩或 `/compact` 触发时,stdout 会加入摘要指令。 |
| 254 | |
| 255 | ## 排障 |
| 256 | |
| 257 | - 保存后当前会话没有变化:Hooks 在会话构建时加载。重启桌面端后才会重新读取配置;`/new` 只开启新对话,不会重新加载 hooks。 |
| 258 | - 项目 hooks 不执行:确认当前是项目工作区、配置保存在 `.reasonix/settings.json`,并重启 Reasonix 重新加载。也可用只读诊断:`reasonix doctor capabilities` 或桌面端 **设置 → 诊断**(见 [能力诊断](./CAPABILITY_DIAGNOSTICS.zh-CN.md)),关注 `hook.invalid_matcher` / `hook.malformed_settings`。 |
| 259 | - `match` 没生效:它只对 `PreToolUse` 和 `PostToolUse` 生效,并且是锚定正则。 |
| 260 | - JSON 报 unknown hook event:事件 key 必须完全等于上表的大小写。 |
| 261 | - hook 输出太长:每路 stdout/stderr 最多捕获 256KB,超出会截断并显示截断提示。 |
| 262 |