Codex 把改动 commit 到了错误分支(甚至直接 main)

Codex 在沙箱当前 checkout 的分支上 commit——有时是 main,有时是上一个任务遗留的 branch。怎么强制每个任务跑在干净的独立分支上。

你给 Codex 派一个小活——“改个 prop 名,顺便把调用点也更新一下”。二十分钟过去,PR 列表里啥也没有。git log 一看 main:agent 的 commit 直接落在 main 上,没分支,没 PR。或者它 push 到了 feature/old-experiment——那是三周前同事用过的分支。或者它先创了新分支,但最后一次 commit 前 checkout 错了,一半工作落在 agent/task-7,另一半落在 agent/task-8

Codex 继承沙箱启动时所处的分支。如果 harness 没有为每个任务强制 pin 一个新分支,所有下游假设——PR 创建、branch protection、review——都会悄无声息地失效。你以为的 “隔离 agent 跑” 其实在和上一个任务共享状态。

这篇覆盖分支为啥会错、怎么强制每任务隔离、commit 已经落错地方时怎么救。

常见原因

1. 沙箱起来时停在上次 checkout 的分支

worker container 被复用。上一个任务跑完留在了 agent/task-42 上。新任务起来,Codex 看见 repo,改文件、commit。Commit 落在 agent/task-42,而不是这次任务对应的新分支。

如何判断:PR 缺失。git log --all --oneline 显示本次任务的 commit 落在某个老任务命名的分支上。worker 复用是源头。

2. AGENTS.md 写了 “在当前分支 commit”

某条好心的指引说 “commit 你的改动”,但没说在哪个分支。Agent 照字面理解,直接用 HEAD。如果 HEAD 是 main,你的工作就上了 main。

如何判断grep -i 'commit on\|push to' AGENTS.md docs/。如果没有明确的 “先创建一个叫 X-Y-Z 的新分支” 规则,agent 不会自己想到。

3. git checkout -b 静默失败,agent 仍然继续

Harness 试着跑 git checkout -b agent/fix-issue-123,但同名分支因为之前的运行已经存在。git checkout -b 报错。Agent 只看 stdout,忽略了 stderr,继续在当前分支上 commit。

如何判断:transcript 里出现 fatal: A branch named 'agent/fix-issue-123' already exists.,但后面照样有 commit。这些 commit 落在 HEAD 当时指向的任何地方。

4. Agent rebase 到 main 后直接在 main 上 commit

Codex 被告知 “和 main 保持同步”。它跑了 git checkout main && git pull,但忘了改完文件前切回任务分支。新 commit 本地落在 main 上;如果 harness 接着 push main,要么撞上 protected branch(最好情况),要么真推上去(最糟糕情况)。

如何判断:transcript 有 git checkout main,后面没有对应的 git checkout agent/...。Commit 的 parent SHA 直接接在 main 的 tip 上。

5. 两个并行 agent 任务共用了同一个 worktree

你同时开了两个任务。两个 worker pod 挂了同一个 persistent volume。两边都跑 git checkout -b,一个成功,但两边都 commit 了。败方的 commit 混进了胜方的分支。

如何判断:同一个分支里有两个无关任务的 commit 交错出现。worker pod 日志显示同一个 volume 上有重叠时间戳。

6. 分支名模板里某个变量没赋值

Harness 用类似 agent/${TASK_ID} 拼分支名。TASK_ID 是空。Agent 跑了 git checkout -b agent/,根据 git 版本,要么报错,要么真创建了一个名字就叫 agent/ 的分支。

如何判断git branch -a 里出现叫 agent/agent、或者干脆 ${TASK_ID} 的分支。模板拼接是源头。

最短修复路径

Step 1:沙箱启动时强制创建新分支

.codex/setup.sh 或 harness 的 task-init hook 里:

#!/usr/bin/env bash
set -euo pipefail

# 不管上一个任务留下啥状态,先 reset 干净
git fetch origin
git checkout main
git reset --hard origin/main
git clean -fdx

# 按本次任务的 ID 创建新分支
BRANCH="agent/${TASK_ID:?TASK_ID must be set}-$(date +%s)"
git checkout -b "$BRANCH"
echo "Working on $BRANCH"

:? 守卫保证 TASK_ID 没赋值时直接 abort,避开空模板那个 bug。时间戳后缀防止同一任务重试时撞名。

Step 2:AGENTS.md 写死分支规则

在文件靠前位置加:

## 分支规则

- 始终在 setup.sh 创建的分支上工作。不许切分支。
- 任务中途不许跑 `git checkout main`
- 不许直接 commit 到 main、master、develop、release/*。
- 要同步 main,从任务分支用 `git merge origin/main`

Codex 会优先采纳它看到的最具体指令。把这条放文件顶部就压过 “commit 你的改动” 这种含糊话。

Step 3:在 server 端禁掉直接 push 到 main

GitHub 或你的 git 托管平台的 repo 设置里:

  • mainmasterdevelop、所有 release/* 加 branch protection
  • 禁止直接 push(只能走 PR)
  • 禁止 force-push
  • 强制 status checks

这是兜底网。即便 Codex 想 push main,server 也会拒。

Step 4:pre-push hook 拒绝 protected branch

.codex/setup.sh 里创建分支之后加:

mkdir -p .git/hooks
cat > .git/hooks/pre-push <<'EOF'
#!/usr/bin/env bash
while read local_ref local_sha remote_ref remote_sha; do
  case "$remote_ref" in
    refs/heads/main|refs/heads/master|refs/heads/develop|refs/heads/release/*)
      echo "ERROR: agent run 不许 push 到 $remote_ref"
      exit 1
      ;;
  esac
done
EOF
chmod +x .git/hooks/pre-push

这一层在沙箱里就拦下,不用等 server 端拒——server 端拒绝有时会让沙箱卡在一个奇怪的半成功状态。

Step 5:commit 步骤里再校验一次分支

Commit 命令包一层校验:

current=$(git rev-parse --abbrev-ref HEAD)
case "$current" in
  agent/*) ;;
  *) echo "拒绝在 $current 上 commit"; exit 1 ;;
esac
git add -A
git commit -m "$1"

中间只要 HEAD 偏离了任务分支,commit 会大声炸出来,而不是悄无声息落到错地方。

Step 6:commit 已经落错地方时怎么救

如果 commit 已经本地落到 main 但还没 push:

# 先把这些 commit 救到新分支
git branch agent/rescue-$(date +%s)

# main 重置回 origin/main
git fetch origin
git reset --hard origin/main

# 切回救出来的分支继续干
git checkout agent/rescue-...

如果 push 已经发生但 main 是 protected——好,push 被拒了,按上面方法删本地 commit 就行。如果 main 没保护、push 真成功了,得用 git revert <range> 加一个跟进 PR。不要 --force push main 想把它擦掉——所有人 clone 都会崩。

Step 7:审计最近的 agent 跑有没有同样问题

# 找看起来孤立的分支(agent 前缀、有 commit、没 PR)
git for-each-ref --format='%(refname:short) %(committerdate:iso8601)' refs/remotes/origin/agent/ \
  | sort -k2 \
  | head -50

有 commit 但没对应 PR 的分支都要怀疑。要么 PR 创建步骤静默失败了,要么分支根本错了。

这事不一定怪你

如果你的 harness 厂商复用 worker pod 时不重置 worktree,你 AGENTS.md 和 pre-push hook 怎么钉都没用——底下的状态还是在漏。给 Codex 服务提供方报 bug;修复点在 worker 生命周期,不在你的 repo。

容易误诊成什么

“PR 创建那步不稳定”。如果没 PR 但有 commit,PR 创建那步八成是正常的——它只是没找到新分支去开 PR,因为 commit 落到了别的地方。先查 branch 再 debug PR creator。

Prevention

  • setup 脚本 reset 到 origin/main,按任务带时间戳后缀创建分支
  • TASK_ID 等所有分支名输入都用 :? 守卫,空值直接 abort
  • AGENTS.md 明确禁止 git checkout main 和直接 commit 到 protected branch
  • server 端给所有保护分支加 branch protection
  • 沙箱里的 pre-push hook 拒绝 push 到 protected ref
  • commit wrapper 校验 HEAD 匹配 agent/* 模式
  • 每周 audit 一次 agent/* 前缀但没 PR 的孤儿分支

FAQ

  • 能不能让 agent 跨任务复用一个分支保留上下文? 不行。跨任务状态会双向泄露——任务 A 的代码出现在任务 B 的 PR 里,reviewer 也没法从 diff 看出 agent 的真实意图。
  • 如果项目本来就是 push 到一个长期存在的集成分支怎么办? 集成分支按 main 待遇处理:加 protection、禁直接 commit、agent 也必须走 PR。

标签: #Codex #AI 编程 #排查