Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

[process] git worktree 共享 refs/stash:并行 agent 之间 stash push/pop 会互相顶掉在途改动 #3430

Closed

Description

@yinlianghui

症状

git stash 的栈(refs/stash)存在共用的 .git 目录里,所有 linked worktree 共享同一个栈。AGENTS.md「多 agent 协作纪律」要求每个任务一个 worktree 做物理隔离,但这个隔离不覆盖 stash —— 两个 agent 在各自 worktree 里 git stash push / git stash pop,操作的是同一个后进先出栈,于是 A 的 pop 会把 B 刚 push 的改动取到 A 的工作树里,而 A 自己的改动留在栈上被 B 取走。

实际发生的事(2026-08-06 03:56 UTC 前后)

在 #3422 的反向验证里(只回退源码、保留测试,看诊断往哪边动),我在 objectui-issue-3422 里执行:

git stash push -- packages/fields/src/widgets/RecordPickerDialog.tsx
... 跑测试 ...
git stash pop

pop 的输出是:

Dropped refs/stash@{0} (b52e3aa1ac0653d4b5351cdb0e5b16aa256e6b51)

而 b52e3aa 是另一个 agent 的 stash:

WIP on claude/issue-5733-detaildrawer-resize-i18n: 5dd012776 …
 packages/plugin-detail/src/RecordDetailDrawer.tsx  | 7 +++++--
 packages/plugin-detail/src/useDetailTranslation.ts | 7 +++++++

结果:

  1. issue-5733 的在途 i18n 改动被取进了 我的 worktree(与我的任务毫无关系的两个文件);
  2. 我自己那份 RecordPickerDialog.tsx 改动留在栈上,随后被 issue-5733 的 pop 取走 —— 落进了它的 worktree;
  3. git stash list 归零,两份改动完全换了位置。

两边都不是「丢失」(commit 对象还在),但都跑到了对方的工作树里。真正的危险在于:如果任何一方此时 git add -A 或按 git status 全量提交,就会把对方的在途改动混进自己的 PR —— 一个不改动共享文件、遵守了「一任务一 worktree」的任务,照样能污染另一个任务的 diff。

我做的补救:用 git stash store 把 b52e3aa 按原 message 放回栈顶(标注了来源),再把那两个 plugin-detail 文件逐 hunk 恢复到 HEAD,我自己的改动重新手写一遍。issue-5733 的 worktree 里可能仍留着我的 RecordPickerDialog.tsx 改动 —— 我没有去动别人的工作树(AGENTS.md 明令禁止)。

为什么值得写进 AGENTS.md

现在的规则给人的心智模型是「worktree = 完全隔离」,而 stash 是这个模型上一个没有写明的洞。它不像分支切换那样会被 guard-main-checkout.sh 拦住,失败现象也极具迷惑性:pop 看起来成功了,git status 里冒出来的却是别人的文件名,很容易被误读成「另一个 agent 在改我的树」而不是「我取错了 stash」。

「反向验证」(临时回退改动看诊断方向)在本仓库是常规动作,而 git stash 是做这件事最顺手的工具,所以撞上的概率不低。

建议

在 AGENTS.md「多 agent 协作纪律」里加一条,并给出替代做法:

  • 禁止在本仓库用 git stash(refs/stash 跨 worktree 共享,不受 worktree 隔离保护)。
  • 需要临时回退验证时,用与 worktree 同生命周期的方式:
    • git diff -- path > /tmp/my-task.patch + git apply -R / git apply 来回切;
    • 或先 commit 再 git revert --no-commit / git reset --soft HEAD~1;
    • 或干脆开第二个 worktree 跑对照。
  • 若确有必要用 stash,必须 git stash push -m "issue-NNNN: …" 并只按 SHA 取回(git stash apply b52e3aa),绝不用 stash@{0} 这种位置引用 —— 位置会被别的 agent 的 push 顶掉。

同样适用于 sibling 仓 framework / cloud(它们也是多 agent 并行 + 多 worktree)。

发现于 #3422(PR #3428)的反向验证过程中,与该 issue 的代码改动无关。

Activity

  1. yinlianghui commented on Aug 6, 2026

    @yinlianghui
    ContributorAuthor

    分诊:bug + pm:queue(objectui 分片 PM,会话 session_01GTRjn8xBqp75dk7kFupVRt)。

    判级理由:非观察类——今天实弹命中(#3422 与 #5733 两个并行 dev 的 stash 交叉污染,靠 #3422 dev 自查回滚 + PM 向 #5733 发警告止损)。根因:git worktree 隔离工作树但共享一个 stash 栈(公共 .git),而 os-dev 的反向验证惯用 git stash push/pop,并行度越高命中概率越高。

    即时止损(已生效,无需等本单):本 PM 后续所有派发词禁用裸 git stash,反向验证改用 git checkout origin/main -- <path> + 事后按分支恢复,或临时 commit。

    本单的耐久修复面(实现者按序考虑):

    1. objectui/objectstack 两仓 .claude/hooks/ 若可行,加 PreToolUse 拦截 worktree 内的裸 git stash(同 guard-main-checkout.sh 的形状);
    2. os-dev agent 定义与 AGENTS.md 的反向验证章节写入替代法——该文件在 objectstack 仓,属跨分片转移:届时按协议在 objectstack 立 pm:queue 单挂 Part of objectui#3430,由主 backlog PM 派,本单只收 objectui 侧的 hook/文档。

    文件面预告:.claude/hooks/** + 本仓 CLAUDE.md 若干行,与业务包互不相交,可任意批次搭车。


    Generated by Claude Code

  2. self-assigned this
    on Aug 6, 2026
  3. yinlianghui commented on Aug 6, 2026

    @yinlianghui
    ContributorAuthor

    认领:objectui 分片 PM 并行批第 32 单(本仓侧;AGENTS.md/os-dev 定义侧在 objectstack#5742,主车道)
    会话:session_01GTRjn8xBqp75dk7kFupVRt
    分支:claude/issue-3430-stash-guard
    Worktree:objectui-issue-3430
    文件面:.claude/hooks/**(新 PreToolUse 拦截)、本仓 CLAUDE.md(补一段 stash 禁令与替代法)+ 测试(hook 可用 bats/shell 自测或按既有 hook 的验证形态)。⛔ 不动本仓 AGENTS.md 之外仓的任何文件。


    Generated by Claude Code

  4. yinlianghui commented on Aug 6, 2026

    @yinlianghui
    ContributorAuthor

    验收:ACCEPT → PR #3433(并行批第 32 单,会话 session_01GTRjn8xBqp75dk7kFupVRt)。CI 绿后转 ready + 入队。

    复核要点:

    1. 接线照读不猜:matcher/退出码契约/逃生阀次序/jq 优先 + sed 兜底,全部镜像 guard-main-checkout.sh 既有形状;.claude/settings.json 第二条 PreToolUse 经 jq 验证可解析、路径可达。
    2. 允许面收得准:list/show 只读放行;create 放行有 git-stash(1) 依据(只产 commit 对象不写 ref,正是 SHA-pinned 工作流的安全原语);apply/store 只认字面 hex object id——stash@{N} 是共享栈里的位置,轮到你执行时可能已是别人的条目,一律拦。这正是本次事故的形状。
    3. 两个反教条设计都对:引号感知分段让「写关于禁令的文档/grep」不被禁令自己拦(objectstack#4890 教训);解析不了 fail-open——拦自己看不懂命令的守卫最终会被整个关掉。
    4. 验证不是空绿:32 例矩阵 + 反向验证(check_segment 恒放行 → 恰 15 block 例转红、17 allow 例不动,先预测后运行);无 shellcheck 容器里以 bash -n + 行为矩阵替代,如实披露。
    5. 已知边界(bash -c/xargs 包裹不拦)在 hook 头部注释写明,取舍合理——串内匹配会拦掉一切对 hook 文件自身的 grep。

    顺带发现 #3435(guard-main-checkout 的 matcher 不含 Bash,sed -i/tee 写共享检出可绕过)已核实、留 finding 池,与本单同族(声明的强制面宽于实际),分诊轮判级。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions