1313- P4:生命周期点位扩展(permission/session/compact/subagent)+ 点位能力矩阵
1414- P5:internal hooks 支持 ` async/async_rewake ` + run 内存通知队列(ephemeral 注入)
1515- P6-lite:user ` http/observe ` hooks(仅观测回调)
16+ - P6:user/repo ` command ` hooks(stdin/stdout JSON 协议)
1617
1718当前未实现能力:
1819
19- - command/ prompt/agent hooks(P6)
20+ - prompt/agent hooks(P6)
2021
2122## P2 user hooks 边界
2223
@@ -32,7 +33,8 @@ P2 仅支持:
3233- ` kind=http + mode=observe ` :允许发送 HTTP 观测回调(不支持 block)
3334- ` http observe ` 默认不携带 metadata(` include_metadata=false ` );即使显式开启也会剥离 ` result_content_preview ` 、` execution_error `
3435- ` http observe ` 回调端点仅允许 loopback 地址(` localhost ` / ` 127.0.0.1 ` / ` ::1 ` ),避免误配为公网外发
35- - external kinds 中 ` command/prompt/agent ` 在 P6-lite 阶段显式拒绝,不会半生效
36+ - ` kind=command + mode=sync ` :允许执行外部命令,通过 stdin/stdout JSON 协议通信(详见下方 P6 章节)
37+ - external kinds 中 ` prompt/agent ` 仍显式拒绝
3638
3739当前(P3)明确不支持:
3840
@@ -105,7 +107,7 @@ runtime 内置 `HookPointCapability` 作为唯一真源,定义每个点位是
105107约束规则:
106108
107109- ` CanBlock=false ` 的点位,hook 返回 ` block ` 会自动降级为观测结果,不中断主链。
108- - ` CanUpdateInput ` 仅作为能力建模;当前阶段不开放输入改写通道 。
110+ - ` CanUpdateInput ` 在 ` user_prompt_submit ` 点位已开放:command hook 可通过 stdout JSON 的 ` update_input ` 字段改写用户输入 。
109111- ` UserAllowed=false ` 的点位拒绝 user/repo 挂载(配置 fail-fast)。
110112
111113### trust gate
@@ -135,6 +137,159 @@ trust store 固定路径:
135137- 绝对路径必须位于 workdir 内
136138- symlink 路径会进行 realpath 校验,禁止绕过
137139
140+ ## P6 command hooks
141+
142+ ` kind=command ` 允许 user/repo scope 通过外部可执行脚本参与 hook 链。
143+
144+ ### stdin 协议
145+
146+ 外部命令通过 stdin 接收单行 JSON:
147+
148+ ``` json
149+ {
150+ "payload_version" : " 1" ,
151+ "hook_id" : " my-hook" ,
152+ "point" : " before_tool_call" ,
153+ "run_id" : " run_abc123" ,
154+ "session_id" : " sess_abc123" ,
155+ "metadata" : {
156+ "tool_name" : " bash" ,
157+ "workdir" : " /path/to/workspace"
158+ }
159+ }
160+ ```
161+
162+ - ` payload_version ` :协议版本号,当前固定 ` "1" ` ,变更 stdin 结构时递增
163+ - ` hook_id ` :hook 配置中的 ` id `
164+ - ` point ` :触发点位名称
165+ - ` metadata ` :经白名单裁剪后的上下文字段(与 builtin/http hook 相同的 allowlist)
166+
167+ ### stdout 协议
168+
169+ 外部命令通过 stdout 返回单行 JSON:
170+
171+ ``` json
172+ {
173+ "status" : " pass" ,
174+ "message" : " optional message" ,
175+ "update_input" : {"text" : " rewritten prompt" },
176+ "annotations" : [" note1" , " note2" ]
177+ }
178+ ```
179+
180+ - ` status ` :必填,` pass ` / ` block ` / ` failed `
181+ - ` message ` :可选,进入 hook event 和 annotation buffer
182+ - ` update_input ` :仅 ` CanUpdateInput=true ` 的点位(当前仅 ` user_prompt_submit ` )允许;格式 ` {"text": "..."} ` 替换用户输入文本
183+ - ` annotations ` :字符串数组,进入 runtime annotation buffer
184+
185+ ### stdout 退化模式
186+
187+ 如果 stdout 不是合法 JSON,handler 退化为 exit code 模式:
188+
189+ - exit 0 → ` pass `
190+ - exit 1 或 2 → ` block `
191+ - 其他 → ` failed `
192+
193+ 原始 stdout 文本作为 ` message ` 。此模式兼容简单脚本(如 ` echo "ok"; exit 0 ` )。
194+
195+ ### 执行模式
196+
197+ #### argv 模式(默认)
198+
199+ ` params.command ` 为字符串数组,直接 exec 不经 shell:
200+
201+ ``` yaml
202+ kind : command
203+ params :
204+ command :
205+ - python3
206+ - /path/to/hook.py
207+ ` ` `
208+
209+ #### shell 模式
210+
211+ ` params.command` 为字符串且 `params.shell: true`,通过 `sh -c`(Unix)/ `powershell -Command`(Windows)执行:
212+
213+ ` ` ` yaml
214+ kind: command
215+ params:
216+ command: "python3 /path/to/hook.py"
217+ shell: true
218+ ` ` `
219+
220+ 单字符串 `params.command` 不设置 `params.shell : true` 会触发配置校验错误。
221+
222+ # ## 环境变量
223+
224+ 命令进程仅注入以下环境变量,不继承宿主环境:
225+
226+ | 变量 | 值 |
227+ |------|------|
228+ | `NEOCODE_HOOK_HOOK_ID` | hook 的 `id` |
229+ | `NEOCODE_HOOK_POINT` | 触发点位(如 `before_tool_call`) |
230+ | `NEOCODE_HOOK_PAYLOAD_VERSION` | `"1"` |
231+
232+ Windows 额外注入 `SystemRoot`、`SystemDrive`、`USERPROFILE`(从宿主环境读取),以确保 TLS 证书加载和运行时基础功能正常工作。
233+
234+ # ## 执行约束
235+
236+ - workdir = 当前 run 的 workspace(`cmd.Dir = workdir`)
237+ - 超时 = hook 配置的 `timeout_sec`(默认 2s)
238+ - 并发限制 = executor 的 `max_in_flight`(默认 128)
239+ - repo scope command hook 受 trust gate 保护
240+ - stdout 大小限制 = 1 MiB;超出视为 `failed`
241+
242+ # ## stderr 处理
243+
244+ 外部命令的 stderr 与 stdout 分离捕获。stderr 不会混入 `message` 字段,仅在命令执行失败(非零 exit code)且 stdout 无可用 message 时,stderr 内容才作为 fallback 追加到结果中。此设计确保 hook 协议输出(stdout JSON)不受调试输出(stderr)干扰。
245+
246+ # ## stdin 字段说明
247+
248+ - ` run_id` / `session_id` 同时出现在 payload 顶层和 `metadata` 中。**顶层字段为权威来源**,`metadata` 中的同名字段为冗余副本(与 builtin/http hook 的 metadata allowlist 一致)。外部脚本应优先读取顶层字段。
249+ - ` payload_version` 当前固定为 `"1"`,变更 stdin 结构时递增。
250+
251+ # ## update_input 与 block 交互
252+
253+ 当 hook 返回 `status : " block" ` 时,` update_input` 不会被应用。阻断优先于输入改写——hook 链在检测到 block 后立即终止,不进入 `applyCommandHookUpdateInput` 逻辑。
254+
255+ # ## 安全:exit code 优先于 JSON status
256+
257+ 当命令以非零 exit code 退出时,stdout 中 JSON 声称的 `status` 字段被忽略。exit code 的映射优先:
258+
259+ - exit 1/2 → `block`
260+ - 其他非零 → `failed`
261+
262+ 此规则防止恶意脚本通过 `{"status":"pass"}` 掩盖实际失败。JSON 中的 `message` 和 `annotations` 仍会被提取(如果 stdout 是合法 JSON)。
263+
264+ # ## 示例
265+
266+ # ### Python
267+
268+ ` ` ` python
269+ #!/usr/bin/env python3
270+ import json, sys
271+
272+ payload = json.loads(sys.stdin.readline())
273+ if payload["metadata"].get("tool_name") == "bash":
274+ json.dump({"status": "block", "message": "bash not allowed"}, sys.stdout)
275+ else:
276+ json.dump({"status": "pass"}, sys.stdout)
277+ print()
278+ ` ` `
279+
280+ # ### Bash
281+
282+ ` ` ` bash
283+ #!/bin/bash
284+ read -r line
285+ tool=$(echo "$line" | jq -r '.metadata.tool_name // empty')
286+ if [ "$tool" = "rm" ]; then
287+ echo '{"status":"block","message":"rm is blocked"}'
288+ else
289+ echo '{"status":"pass"}'
290+ fi
291+ ` ` `
292+
138293# # 可观测性
139294
140295runtime 会透传 hooks 生命周期事件:
0 commit comments