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