生成日期: 2026-08-10 状态: v0.1 实现完成,待端到端验证
更新(v0.4,2026-08-11):哲学从"强制 worktree"改为"默认主副本开放,按需隔离"。 本文档下方描述的是 v0.1 的原始设计(强制模型)。相对 v0.1 的行为差异:
- 无绑定时 Write/Edit/Read 主 checkout 从 🔴拦截 → ✅放行(默认开放);
- 无绑定时本地
git merge/rebase/pull/checkout放行(push 到 master/main 仍拦截);- 绑定只来自本会话
enter(+ subagent 继承父链);state.json不再作为绑定真值;- 跨副本写入、写
.git、push 到 master/main、删 worktree 分支——始终拦截(安全网)。 下方的决策表与目标表述保留为 v0.1 历史;以 README 与 SKILL.md 为准。
为 ZCode 构建一个 worktree 强制插件,解决 agent 在 worktree 模式下的两大痛点:
- 路径漂移——agent 以为自己写到了 worktree,实际写到了主 checkout(或反之);
- 遵循度——即使有 SKILL 指导,agent 也可能忘记进入 worktree 就直接改主分支。
| 层 | 机制 | 作用 | 来源 |
|---|---|---|---|
| 透明重写(主) | PreToolUse + Form 3 updatedInput |
agent 无感知地重写路径 R→W | opencode 思路 |
| 拦截防御(辅) | 同 hook 内 deny / exit 2 | 写保护 + 危险 git 操作拦截 | kimi 思路 |
两者融合:重写覆盖不了的场景(写其他副本、危险 git 操作)才拦截。
实测(2026-08-07 冒烟测试):hook 输出
{"hookSpecificOutput":{"hookEventName":"PreToolUse","updatedInput":{...}}}Write 的 file_path 被改写后,文件实际落到改写后路径,原路径未变。updatedInput 是整体替换,
替换后过工具 runtime schema 校验(safeParse),所以字段名必须匹配。
源码位置:bundle zcode.cjs,函数 HNi(line 2900)、CQ/udn(line 2417/2451)。
| 工具 | 路径字段 | 其它 | strict |
|---|---|---|---|
| Write/Edit/Read | file_path |
— | 否 |
| Glob/Grep | path (optional) |
pattern |
否 |
| Bash | (无路径字段) | command |
是 |
| ApplyPatch(v0.4.6) | operation.path(嵌套) |
callId、operation.type/diff |
— |
Bash 无 cwd 字段且 strict——无法通过 hook 改写其工作目录,这是 ZCode 的硬限制。 故 Bash 只做拦截,不重写。
ApplyPatch(v0.4.6 补充,源码确证 zcode.cjs 0.16.9):OpenAI responses 提供方的补丁式
写工具。引擎把 apply_patch_call 转成内部 tool-call(toolName:"ApplyPatch",
input:{callId, operation:{type:"create_file"|"update_file"|"delete_file", path, diff?}})。
hook 触发靠引擎 matcher 别名表 ApplyPatch→[Write,Edit](matchValues 展开后命中
Write/Edit 即触发,stdin 上仍是真实名)——因此 matcher 不写 ApplyPatch 也能触发;但分发层
必须有自己的分支(v0.4.5 及以前缺失 → 绑定态静默放行)。重写只替换 operation.path,
其余字段原样保留以过 schema 校验。GLM 等走 Write/Edit 的提供方不产生该工具。
session_id/sessionId、cwd、tool_name/tool_input、hook_event_name、permission_mode。
无 parent_session(关键,见 §4.1)。
| 管道 | bundle 函数 | 支持变量 | user_config? |
|---|---|---|---|
| Skill 正文 | E5i (line 2754) |
仅 CLAUDE_SKILL_DIR/ZCODE_SKILL_DIR |
否 |
| Hook command/args | NTe (line 2943) |
SESSION/PROJECT/PLUGIN_ROOT/PLUGIN_DATA | 否 |
| MCP server env | Vm |
上述 + env + user_config.* |
是 |
关键约束:
${ZCODE_PLUGIN_ROOT}在 hook command/args 会展开,但在 skill 正文不展开 → SKILL 用相对路径。${user_config.*}只 MCP 可达,hook 读不到 → 配置走 sidecar 文件。
v1 计划在实现前经两轮 Explore agent 审计,发现 5 个缺陷并修正:
v1 假设:用 session_id 做 key,支持并行多 worktree。
证据:子代理(Agent/Task 派生)有独立 session_id(sess_subagent_agent_*),
PreToolUse stdin 无 parent_session(调用链 wxn→xcn→xzt)。session 级绑定对子代理不可继承。
修正:改用 git common dir 仓库级单活动绑定(状态存 common dir,子代理 cwd 落同仓库自动继承)。
放弃并行多 worktree(且 Bash cwd 不可重写已削弱其价值)。
v1 假设:hook 用 "command":"node"。
证据(后被部分推翻):Explore agent 当时报告"ZCode 不向 hook 暴露内部 node"。
v2 修正(已被推翻,见下方更新):当时改用 Python(C:/Python314/python.exe)。
2026-08-10 更新:此判断被用户质疑后复核,实测发现 (a) 系统装有 node v24.15 且在 PATH, (b)
ELECTRON_RUN_AS_NODE=1 ZCode.exe可作为 node v24.14 运行 TS 脚本。 原"node 不可用"判断是基于 Explore 报告未亲自验证的疏漏。插件已整体重写为 TypeScript(.mjs), hooks.json 用command:"node"(依赖 PATH),不再硬编码机器路径。详见 [[verify-runtime-assumptions-before-tech-choice]]。
v1 假设:SKILL 用 ${ZCODE_PLUGIN_ROOT}/scripts/wt.mjs。
证据:skill 渲染管道 E5i 只展开 SKILL_DIR。
修正:SKILL 用相对路径 ../../scripts/wt.mjs(TS 版),依赖 skill base 自动追加行(生态先例:superpowers/brainstorming)。
v1 假设:manifest userConfig 被 hook 读取。
证据:hook 变量是封闭枚举(NTe),无 user_config。
修正:走 sidecar <repo>/.zcode/worktree-guard.json。
v1 假设:"hooks":"hooks/hooks.json"。
证据:hooks/hooks.json 已按约定自动发现(引擎 listPluginHookSources 硬编码探测
join(root,"hooks","hooks.json");manifest 字段仅用于声明额外 hook 文件,按 realpath 去重)。
修正:manifest 省略 hooks 字段。
v0.4.6 更新:重新显式声明
"hooks":"hooks/hooks.json"——纯自文档化(plugin-creator 规范形态),引擎行为不变(自动发现 + 显式声明 realpath 去重,不双注册)。
| 场景 | 行为 |
|---|---|
| 有活动 worktree,Write/Edit/Read 主 checkout 路径 | 🔄 重写 R→W |
| 有活动 worktree,ApplyPatch 主 checkout 路径(v0.4.6) | 🔄 重写 operation.path |
| 有活动 worktree,Glob/Grep 无 path | 🔄 注入 path=W |
| 有活动 worktree,Glob/Grep path 在主根下 | 🔄 重写 R→W |
| 有活动 worktree,写副本内路径 | ✅ 放行 |
| 有活动 worktree,写其他副本 | 🔴 拦截 |
写 .git |
🔴 拦截 |
| 无活动 worktree,Write/Edit 主 checkout | 🔴 拦截(写保护) |
| 无活动 worktree,Read 主 checkout | ✅ 放行 |
| 受保护分支上 merge/rebase/pull | 🔴 拦截 |
| push 到 master/main | 🔴 拦截 |
| 副本内 checkout 到受保护分支 | 🔴 拦截 |
| 删 worktree 分支 | 🔴 拦截 |
| authorize-main 期间 | ✅ 全放行 |
| 仓库外/非 git | ✅ 放行 |
<git-common-dir>/worktree-guard/
state.json # {active, path, branch, base, entered_at} 仓库级单活动
override.json # {allow_main_writes, reason, created_at}
bases.json # {"<worktree-branch>": "<base>"}
位于 git common dir,天然不进版本库、所有 worktree 共享、重启自动恢复。
- fail-open:hook 脚本异常/超时放行,绝不阻塞 agent。
- Bash 不重写:ZCode 硬限制,Bash 只拦危险 git 操作。agent 的
echo > file类操作不会自动重写, 但 Write/Edit 工具会——SKILL 已指导优先用工具写文件。 - Bash 正则限制:只匹配
git直接开头的简单命令,cd x && git merge类组合可能绕过 (与 kimi 一致的已知限制)。 - MCP 工具写盘不在守卫范围(v0.4.6 记录):MCP 工具(如 node-repl
js)的 tool_input 无文件路径语义、无法静态重写——等同 Bash 边界,靠 SKILL 纪律(副本相对路径)约束。 ApplyPatch 已于 v0.4.6 纳入(见 §3.2)。 - 无热重载:hook 注册在 session bootstrap 完成,改 hooks.json 或安装插件后须重启 ZCode。 但 hook 脚本逻辑改完即生效(每次调用是新进程)。
- kimi 参考:
F:\workspace_2\worktree-guard\(wt.py / guard_worktree.py / SKILL.md,逻辑直接复用) - opencode 参考:
F:\workspace_2\opencode-worktree-guard\docs\design.md(重写思路、拦截规则) - ZCode 契约证据:
C:\Users\Administrator\AppData\Local\Programs\ZCode\resources\glm\zcode.cjs - skill 相对路径先例:
claude-plugins-official\superpowers\6.1.1\skills\brainstorming\visual-companion.md - PreToolUse 改写能力:本会话冒烟测试实测