Uh oh!
There was an error while loading. Please reload this page.
chore: 补上 Bash 侧的 worktree 强制,堵住 sed -i / tee / 重定向绕过主 checkout 守卫 - #3452
Merged
Conversation
guard-main-checkout.sh 只注册在 "Edit|Write|NotebookEdit" matcher 上,同一个写操作 换成 Bash 就完全不经过钩子:sed -i、cat > path <<EOF、tee、printf ... > path 都能 静默改共享主 checkout,而 CLAUDE.md / AGENTS.md 宣称「PreToolUse 钩子强制此规则」。 声明的强制范围 > 实际的强制范围(objectui#3435,与 #3430 同族)。 新增 guard-main-checkout-bash.sh,挂到已有的 Bash matcher 上,与 guard-shared-stash.sh 同形状:jq 为主 + sed 兜底解析 payload、引号感知分段、exit 0 放行 / exit 2 拦截并把理由 写到 stderr、解析不了一律 fail open。仓库判定直接沿用 guard-main-checkout.sh 的逻辑 (最近存在祖先目录 -> git rev-parse --git-dir -> */worktrees/* 放行、非仓库放行), 两个钩子对「共享 checkout」的定义因此不可能分歧;逃生舱沿用同一个 OS_ALLOW_MAIN_EDITS=1, 不另起变量。 精确优先于召回:只认 > / >> 重定向、sed -i、perl -i、tee、cp、mv、rm、touch 这些能 高置信度读出目标路径的写法;bash -c、xargs、node -e / python -c、含 $VAR 或通配符的 目标、payload 缺 cwd 时的相对路径,一律放行并在注释里写明是有意留的口子。读操作 (grep / cat FILE / ls / git grep)永远不拦。 除分段外多加一层 heredoc 剥离:cat > /tmp/notes.md <<EOF 的正文是文档不是命令, 只靠按换行分段会把正文里举例的 sed -i 当成真命令拦下来(objectstack#4890 的教训)。 自测矩阵 .claude/hooks/guard-main-checkout-bash.selftest.sh:自建临时 git 仓库 + linked worktree + 非仓库目录,70 例全绿,不联网不构建。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
The latest updates on your projects. Learn more about Vercel for GitHub. |
yinlianghui
marked this pull request as ready for review
August 6, 2026 05:43
Uh oh!
There was an error while loading. Please reload this page.
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 freeto 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#3435
问题
.claude/settings.json里 guard-main-checkout.sh 只注册在Edit|Write|NotebookEditmatcher 上,所以它只看得见工具化的文件写入。同一个写操作换成 Bash 就完全不经过钩子:这些命令在共享主 checkout 里一声不响地改文件,而 CLAUDE.md / AGENTS.md 给每个 agent 的心智模型是「PreToolUse 钩子强制此规则」。声明的强制范围大于实际的强制范围 —— 与 #3430(stash 共享栈)同族。
做法
新增
.claude/hooks/guard-main-checkout-bash.sh,挂到 #3433 已经建好的 Bash matcher 上(与 guard-shared-stash.sh 并列,任一 exit 2 即拦截)。与 guard-shared-stash.sh 同形状:
split_segments仓库判定直接沿用 guard-main-checkout.sh 自己的逻辑,两个钩子对「共享 checkout」的定义因此不可能分歧:取目标路径最近的存在祖先目录 →
git rev-parse --git-dir→ 非 git 仓库(/tmp、scratchpad、$HOME dotfiles)放行 → 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。读操作永远不拦:
grep/cat FILE/ls/git -C main grep产生不了写目标。worktree 的建立/拆除命令(git worktree add|remove、pnpm install)同理,已在验证里逐条确认。有意留的口子(脚本注释里逐条写明)
bash -c '...'、xargs、ssh host '...'、make、以及会写文件的脚本 —— 与 guard-shared-stash.sh 给自己写明的边界是同一条node -e、python3 -c、awk —— 目标不是 shell token,没有东西可读$VAR、反引号、命令替换、*、?、~)—— 不执行就解析不出来cd被当成粘性的 —— 分段不报告自己是按哪个分隔符切的这些是有意为之而不是疏忽:真心要写的人本来就有
OS_ALLOW_MAIN_EDITS=1,而这个钩子的目标是 agent 干活干到一半反射性敲出来的sed -i,不是存心绕过的人。写「关于这条禁令」的东西不能触发禁令
两层:
'...'/"..."里的>或sed -i是字面文本。grep -n "sed -i" .claude/放行。cat > /tmp/notes.md << EOF的正文是文档不是命令。分段是按换行切的,光靠分段这一层会把正文里举例的sed -i ...当成真命令拦下来。这正是 objectstack#4890 的教训:写规则的那个 PR 不能被自己写的规则拦住。引入 heredoc 的那一行自己的重定向照样算数 —— 自测矩阵里有成对的 allow / block 两例钉住这个区别。
自测
.claude/hooks/guard-main-checkout-bash.selftest.sh,与 #3433 的自测同形状,但自建 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 例)。guard-shared-stash 的自测同步复跑,32 例仍全绿。
反向验证
先定方向再跑:能产出拦截的只有
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 条是手写的绝对路径断言,自带文案),allow 断言零位移。方向与数量都与预测一致 —— 说明这些 block 用例是真的由该判定产出的,不是因为「什么都没产出」而碰巧通过。其它
bash -n两个脚本均通过;jq .校验 settings.json 合法。grep -naP扫 C0 控制字符区间(NUL 到 US,除去制表/换行/回车),干净。写这条时本身踩了一次 PR #5140 的坑 —— 描述控制字符的那行被工具实体化成了真的 NUL 和 0x1F 字节,已按「只描述转义、不粘贴字节」改掉并复扫。.claude/hooks/与.claude/settings.json。Generated by Claude Code