chore(devx): 补上 Bash 侧的 worktree 强制,堵住 sed -i / tee / 重定向绕过主 checkout 守卫 - #5879
Merged
Conversation
… 守卫 (#5790) `guard-main-checkout.sh` 只注册在 `Edit|Write|NotebookEdit` matcher 上,所以它只 看得见工具化的文件写入。同一个写操作换成 Bash 就完全不经过钩子: `sed -i 's/foo/bar/' packages/spec/src/x.ts`、`cat > path` 加 heredoc、`tee`、 `printf ... > path` 都能在共享主 checkout 里一声不响地改文件,而 CLAUDE.md 与 AGENTS.md Prime Directive #11 给每个 agent 的心智模型是「PreToolUse 钩子强制此 规则」。声明的强制范围大于实际的强制范围。 移植 objectui 的成品(objectstack-ai/objectui#3452),逻辑逐例保持一致以免两仓 守卫漂移,只本地化 issue 引用、示例路径与自测里的包名。本仓没有 objectui 的 guard-shared-stash.sh,故 Bash matcher 是新建条目而非并入既有数组;既有 `Edit|Write|NotebookEdit` 条目一字未动。 仓库判定沿用 guard-main-checkout.sh 自己的逻辑 —— 两仓该文件字节相同,故判定 零适配即可移植,两个钩子对「共享 checkout」的定义因此不可能分歧:取目标最近的 存在祖先目录 -> `git rev-parse --git-dir` -> 非 git 仓库(/tmp、scratchpad、 $HOME dotfiles)放行 -> git-dir 命中 `*/worktrees/*` 放行 -> 其余(共享主 checkout、任何 sibling 仓库)拦截。逃生舱沿用同一个 `OS_ALLOW_MAIN_EDITS=1`: 同一条规则,同一个开关。 精确优先于召回:在任意 shell 命令里认出写目标本质上是启发式,而一个会拦住自己 看不懂的活的钩子迟早被关掉,关掉之后它什么也守不住。所以只认能高置信度读出目标 的写法(`>` / `>>` 含 fd 前缀、`sed -i`、`perl -i`、`tee`、`cp`、`mv`、`rm`、 `touch`),读操作永不拦,包装调用(`bash -c`、`xargs`、`node -e`)、含展开或 通配的目标、无 cwd 的相对路径一律 fail open —— 脚本注释里逐条写明这些是有意留 的口子而非疏忽。 写「关于这条禁令」的东西不能触发禁令(#4890 的教训):引号感知的分段与词法切分 让 `'…'` / `"…"` 里的 `>` 或 `sed -i` 是字面文本,heredoc 正文在分析前整段剥离 (正文是文档不是命令,而分段是按换行切的)。引入 heredoc 那一行自己的重定向照样 算数,自测里有成对的 allow / block 两例钉住这个区别。 CLAUDE.md 与 AGENTS.md Prime Directive #11 的钩子描述句同步补上 Bash 面,并写明 精确优先与 fail open —— 声明范围与实际范围重新对齐。 自测 `.claude/hooks/guard-main-checkout-bash.selftest.sh`:自建 fixture(临时 git init 的主仓库 + 它的 linked worktree + 一个非仓库目录),不依赖跑在哪个 checkout 上,70 例全绿。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GX3sL71LFq8m2usg6VqTSE
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
os-zhuang
marked this pull request as ready for review
August 6, 2026 10:58
os-zhuang
enabled auto-merge
August 6, 2026 10:58
This was referenced Aug 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #5790
问题
.claude/settings.json里guard-main-checkout.sh只注册在Edit|Write|NotebookEditmatcher 上,所以它只看得见工具化的文件写入。同一个写操作换成 Bash 就完全不经过钩子:这些命令在共享主 checkout 里一声不响地改文件,而 CLAUDE.md 与 AGENTS.md Prime Directive #11 给每个 agent 的心智模型是「A PreToolUse hook enforces this」。声明的强制范围大于实际的强制范围 —— 多 agent 并行改同一个共享 checkout 正是 worktree-first 规则要防的事,这条路径上它没被防住。
做法:移植而非重新设计
objectui 侧已经把这件事做完了(objectstack-ai/objectui#3452),本 PR 移植成品,逻辑逐例保持一致以免两仓守卫漂移,只本地化三处:issue 引用、注释里的示例路径、自测里的
pnpm --filter包名。新增
.claude/hooks/guard-main-checkout-bash.sh,挂 新建的 Bash matcher —— 本仓没有 objectui 的guard-shared-stash.sh,所以是新建条目而非并入既有数组;既有Edit|Write|NotebookEdit条目一字未动。仓库判定沿用
guard-main-checkout.sh自己的逻辑。两仓该文件diff为空(字节相同),所以判定零适配即可移植,两个钩子对「共享 checkout」的定义因此不可能分歧:取目标最近的存在祖先目录 →git rev-parse --git-dir→ 非 git 仓库(/tmp、scratchpad、$HOMEdotfiles)放行 → git-dir 命中*/worktrees/*(linked worktree)放行 → 其余(共享主 checkout、任何 sibling 仓库)拦截。逃生舱沿用同一个
OS_ALLOW_MAIN_EDITS=1,不另起变量:同一条规则,同一个开关。精确优先于召回
在任意 shell 命令里认出写目标本质上是启发式,误拦风险远高于 Edit 的
file_path;而一个会拦住自己看不懂的活的钩子迟早被关掉,关掉之后它什么也守不住。所以只认能高置信度读出目标路径的写法:>/>>重定向(含2>这类带 fd 前缀的)、sed -i、perl -i、tee、cp、mv、rm、touch。读操作永远不拦。每个 agent 每天都要跑的命令逐条实测未误拦:
git worktree add|remove、git fetch|status|log、pnpm install、flock ... -c "pnpm --filter ... test"、node scripts/check-nul-bytes.mjs、cat/grep/ls/git -C main grep。有意留的口子(脚本注释里逐条写明)
bash -c '...'、xargs、ssh host '...'、make、以及会写文件的脚本node -e、python3 -c、awk —— 目标不是 shell token,没有东西可读$VAR、命令替换、*、?、~)—— 不执行就解析不出来cd被当成粘性的 —— 分段不报告自己是按哪个分隔符切的这些是有意为之而不是疏忽:真心要写的人本来就有
OS_ALLOW_MAIN_EDITS=1,而这个钩子的目标是 agent 干活干到一半反射性敲出来的sed -i,不是存心绕过的人。解析不了一律 fail open —— guard 是防呆不是安防,误杀合法命令的代价高于漏网。写「关于这条禁令」的东西不能触发禁令
两层(#4890 的教训:写规则的那个 PR 不能被自己写的规则拦住):
'...'/"..."里的>或sed -i是字面文本。grep -n "sed -i" .claude/放行。sed -i ...当成真命令拦下来。引入 heredoc 的那一行自己的重定向照样算数 —— 自测矩阵里有成对的 allow / block 两例钉住这个区别。
声明 = 强制
CLAUDE.md与AGENTS.mdPrime Directive #11 的钩子描述句同步补上 Bash 面,并写明「精确优先、读不拦、解析不了就放行,所以规则仍然大于钩子」。AGENTS.md 那句是权威声明,与 CLAUDE.md 是同一句话的两处副本 —— 只改一处会让主声明继续错着。自测矩阵
.claude/hooks/guard-main-checkout-bash.selftest.sh—— 自建 fixture:临时git init的主仓库 + 它的 linked worktree + 一个非仓库目录,所以矩阵不依赖跑在哪台机器、哪个 checkout 上。不联网、不构建。覆盖:共享主 checkout 的各种写法拦截(16 例)、经分隔符/环境变量前缀/绝对 argv0/
cd到达(6 例)、同样的写法落在 linked worktree 放行(8 例)、仓库外(/tmp、非仓库目录、/dev/null)放行(5 例)、读操作放行(10 例)、写「关于禁令」的东西(7 例)、有意留的口子(6 例)、逃生舱(2 例)、payload 解析不了 fail open(5 例)、无 jq 兜底(5 例)。实测四象限(对真实 checkout,dry:钩子只判决 payload,从不执行命令)
钩子从不执行被判决的命令,所以拿主 checkout 的路径喂 payload 本身就是 dry 的;跑完复查
git -C /home/user/objectstack status --porcelain为空,主检出未被碰过。sed -i相对路径、>绝对路径、pnpm build | tee、cd main && touchrepo: /home/user/objectstacksed -i "s/a/b/" package.json@/home/user/objectuiobjectuiOS_ALLOW_MAIN_EDITS=1bash -c "..."、$PKG/...、*.ts、node -e、xargs、空 payload反向验证(先定方向再跑)
能产出拦截的只有
target_is_shared_checkout这一个判定,把它改成恒不命中,只可能减少拦截 —— 这里没有任何下游按数量判定的 gate,所以不存在 PR #5046 那种「删掉分支反而多出诊断」的方向。预测:27 条 block 断言全部翻成 allow,43 条 allow 断言一条不动。在 scratch 副本上把该函数改成恒
return 1后实跑:27 条失败全部是 block 翻 allow(26 条走
expect打印want=block got=allow,第 27 条是手写的绝对路径断言,自带文案),want=allow got=block零条,allow 断言零位移。方向与数量都与预测一致 —— 说明这些 block 用例是真的由该判定产出的,不是因为「什么都没产出」而碰巧通过。生效时点
PreToolUse 钩子在会话/agent 启动时加载,所以本 PR 合并后已在跑的 agent 不受影响,新会话与新 dispatch 的 agent 才开始受这条 Bash 面强制 —— 这是预期行为,不是漏配。
其它
bash -n两个脚本均通过;JSON.parse校验.claude/settings.json合法(两条 PreToolUse 条目按序解析出来)。仓内无shellcheck,故未跑。node scripts/check-nul-bytes.mjs绿(新文件已 tracked 后复跑,进入扫描面:5699 个文件)。改动文件另做超出门禁的控制字符自查(C0 去掉 TAB/LF/CR,加 DEL),干净。100755入库(可执行位)。skip-changeset标签路线。.claude/与两个.md声明句:零.ts、零packages/、零 workflow、零content/docs/releases/。Generated by Claude Code