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

Codex 把活落到了 main、落到遗留分支、或拆成两半——因为云端任务默认用仓库的默认分支,本地 worktree 又会撞名。怎么强制每个任务跑在干净的独立分支上,以及 commit 已经落错地方时怎么救回来。

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

最快的修法: 让每个任务都从干净的默认分支出发、每次都拉一个全新的任务分支,再在 server 端给 main 加保护,这样误推也会被拒。云端 Codex 里这意味着显式设置任务的 base branch(截至 2026 年 6 月,云端任务默认用仓库的默认分支);本地则意味着每个任务一个 Git worktree,外加一个在 agent 动手之前先 reset 再建分支的 setup 脚本。下面逐个讲清每种原因、具体命令,以及 commit 已经落错地方时怎么救。

先搞清楚你跑的是哪种 Codex

这两种形态的翻车方式不一样,先确认你说的是哪一个:

形态工作发生在哪默认分支行为最常见的”落错分支”原因
云端 Codex(从 chatgpt.com/codex 或 GitHub 集成发起的任务)每个任务一个全新隔离的 microVM,跑完即销毁默认 checkout 仓库的默认分支,除非你选了 base branch,或在某个 PR 上 @codex fixbase branch 默认成了 main@codex fix 跑在默认分支而不是 PR 分支上
本地 Codex app / CLI(在你机器上跑 codex你的 checkout,或每个任务一个 Git worktree继承当前 checkout 所在的分支UI 显示一个分支,agent 的 tool call 实际在另一个 workdir 跑;worktree 名字撞了

如果没 PR 但有 commit,那几乎一定是 base-branch 或 worktree 的问题,而不是 PR 那步不稳定。先查分支。

常见原因

1. 云端任务用了默认 base branch

云端任务会起一个全新容器,“checks out your repo at the selected branch or commit SHA”。如果你没选 base branch,它就默认用仓库的默认分支(大多数仓库是 main)。agent 的 commit 只有在你点击开 PR 时才会变成一个新分支;如果 PR 那步被跳过或失败了,活就等于被搁在了 base 上。

如何判断:任务摘要里有 diff,但你从没点过 “Create PR”;或者开出来的 PR 目标是 main,而你想要的是某个 release 分支。任务的环境设置里 base branch 没填或是 main

2. 本地 UI 显示一个分支,agent 却在另一个里干活

本地 Codex app 里,看得见的提示符可能停在一个目录/分支上,而 agent 的 tool call 用的是另一个有效工作目录。你以为活落在 agent/task-12 上,agent 其实在一个 worktree(甚至另一个 repo)里 commit。这是已知问题(openai/codex #23140,是 #21712 的重复),截至 2026 年 6 月仍未修。

如何判断:UI 头部显示的分支上没有新 commit,但 git log --all --oneline 能在另一个 worktree 路径或分支上找到你的改动。跑 git worktree list,逐个看每个路径的 HEAD

3. git checkout -b 撞上了已存在的分支,agent 仍然继续

setup 脚本跑了 git checkout -b agent/fix-issue-123,但同名分支因为之前的运行已经存在,或者这个名字已经被另一个 worktree 占用了。Git 拒绝(git worktree 强制一个 worktree 一个分支)。命令在 stderr 上报了错;agent 只看 stdout,于是在当前分支上继续 commit。

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

4. Agent 同步完 main,忘了切回来

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

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

5. AGENTS.md 写了 “commit 你的改动”,但没分支规则

某条好心的指引说 “commit 你的改动”,但没说在哪个分支。Codex 会优先采纳它手头最具体的指令;没有分支规则时它就用 HEAD。如果 HEAD 是 main,你的工作就上了 main。

如何判断grep -in 'commit\|branch\|push' AGENTS.md 找不到 “先创建一个新分支” 这类规则。agent 不会自己想到。

6. 两个并行任务共用了 worktree 或 volume

你同时开了两个任务,两边挂了同一个 persistent volume(自建 harness)或同一个 worktree 路径。它们在 git checkout -b 上抢;一个赢了,但两边都 commit 了,败方的 commit 混进了胜方的分支。

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

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

setup 脚本用 agent/${TASK_ID} 拼分支名,而 TASK_ID 是空。git checkout -b agent/ 根据 Git 版本,要么报错,要么真创建一个名字就叫 agent/ 的分支。

如何判断git branch -a 里出现叫 agent/agent、或干脆 ${TASK_ID} 的分支。

最短修复路径

Step 1:云端 Codex,在任务上设好 base branch

发起任务之前,显式设好 base branch,别信默认值:

  • 在 Codex 网页/任务 UI 里,base branch 显示在仓库选择器旁边。把它从仓库默认改成你真正想要的 PR 目标分支。
  • GitHub 上用 @codex 时,在 PR 本身 上评论(不是在 issue 或仓库上),这样它才会跑在 PR 分支上。如果你在 issue 上评论,它会从默认分支拉分支。
  • agent 跑完后点 Create PR,合并前先确认 PR 的 base branch 没错。云端 Codex 会帮你命名 head branch,你只需要选 base。

这能消掉云端最大的单一原因:base 没填,被悄悄当成了 main

Step 2:本地 Codex,每个任务一个 worktree

Git 强制一个 worktree 一个分支,而这恰好就是你要的隔离。先建 worktree,再在里面起 Codex(截至 2026 年 6 月,stable 版还没有 --worktree flag,所以手动来):

# 从最新的 main 拉一个独立 worktree,挂在全新分支上
git fetch origin
git worktree add -b "agent/task-123" ../wt-task-123 origin/main
cd ../wt-task-123
codex

每个 worktree 有自己的 HEAD,所以第二个任务不可能误落到这个分支上。跑完用 git worktree list 确认 agent 落在你预期的地方。想给每个 worktree 完全独立的 Codex 配置,把 CODEX_HOME 设成 worktree 本地的路径。

Step 3:setup.sh 里先 reset 干净再建分支

对任何在复用 checkout 上启动的 harness,在 .codex/setup.sh(或 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 建新分支;TASK_ID 为空就 abort
BRANCH="agent/${TASK_ID:?TASK_ID must be set}-$(date +%s)"
git checkout -b "$BRANCH" || git checkout -B "$BRANCH"
echo "Working on $BRANCH"

:? 守卫保证 TASK_ID 没赋值时直接 abort,避开空模板那个 bug。时间戳后缀防止重试时撞名,checkout -B 在残留分支还在时把它重置掉。

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

Codex 会优先采纳它看到的最具体指令,所以把这条放文件顶部:

## 分支规则

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

放顶部的具体规则压过 “commit 你的改动” 这种含糊话。

Step 5:在 server 端禁掉直接 push 到保护分支

这是上面全失效时的兜底网。在 GitHub 仓库设置里,给 mainmasterdeveloprelease/* 加一条 branch protection rule(或 ruleset):

  • 合并前必须走 pull request(挡掉直接 push)
  • 禁止 force push
  • 强制 status checks 通过

即便 Codex 想 push 到 main,server 也会拒。具体开关见 GitHub 的 branch protection 文档

Step 6:加一个拒绝保护分支的 pre-push hook

在 server 看到之前,就在沙箱层把坏 push 拦下。在 .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 端拒绝有时会让沙箱卡在一个奇怪的半成功状态,所以本地先炸更干净。

Step 7:commit 步骤里校验 HEAD

把 agent 的 commit 包一层,HEAD 一旦偏离任务分支就大声炸出来:

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 会直接 abort,而不是悄无声息落到错地方。

commit 已经落错地方时怎么救

如果 commit 本地落到了 main,但 还没 push

# 先把这些 commit 救到新分支(其实就是让一个 ref 指向 HEAD)
git branch "agent/rescue-$(date +%s)"

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

# 切回救出来的分支继续干
git checkout "agent/rescue-..."   # 用上面打印出来的名字

如果 push 已经发生

  • main 有保护:你的 push 被拒了,那就用上面的 reset 删掉本地 commit。branch protection 要的就是这个结果。
  • main 没保护、push 真成功了:用 git revert <oldest_bad>^..<newest_bad> 加一批回退 commit,再开一个正常 PR。不要main git push --force 去擦掉它们——那会重写共享历史,所有人 clone 都会崩。

如果是 本地 commit 落进了错误的 worktree,把它搬走:在正确的 worktree 里跑 git cherry-pick <sha>,再在错的那个里跑 git reset --hard HEAD~1

怎么确认修好了

  1. 重新跑任务,立刻查 git rev-parse --abbrev-ref HEAD(或 git worktree list)——应该在 agent/<task> 上,绝不是 main
  2. agent commit 之后,git log --all --oneline --decorate 应该只在任务分支上看到这些 commit。
  3. 云端任务,打开 PR 确认 headbase 分支都是你想要的,再合并。
  4. 审计最近的跑有没有同样问题:
# agent 前缀的分支,最旧的在前——有 commit 但没 PR 的都可疑
git for-each-ref --format='%(refname:short) %(committerdate:iso8601)' \
  refs/remotes/origin/agent/ | sort -k2 | head -50

这事不一定怪你

如果自建或厂商的 harness 复用 worker pod 时不重置 worktree,你 AGENTS.md 规则和 pre-push hook 怎么钉都救不了——底下的状态还是在漏。给 harness 的负责方报 bug;修复点在 worker 生命周期。本地那个 UI 和 workdir 对不上的问题(#23140 / #21712)同样是上游 bug;在它修好之前,靠显式 worktree 和 Step 7 的 HEAD 校验顶着。

防范清单

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

FAQ

  • 云端任务为啥落到 main 而不是分支? 云端任务默认 checkout 仓库的默认分支,head 分支只有在你开 PR 时才创建。在任务上设好 base branch,并点 Create PR。截至 2026 年 6 月,默认值确实就是你仓库的默认分支。
  • @codex fix 改错分支了,为啥? 在 PR 本身上评论,别在 issue 或仓库上评论。从 issue 发起,Codex 会从默认分支拉分支;从 PR 发起,它才跑在那个 PR 的分支上。
  • UI 显示我的分支,但 commit 不见了,它们去哪了? 本地 app 可能在和提示符不同的有效 workdir 里跑 tool call(#23140)。跑 git worktree listgit log --all --oneline 找到它们,再 cherry-pick 进正确的 worktree。
  • 能不能让 agent 跨任务复用一个分支保留上下文? 不行。跨任务状态会双向泄露——任务 A 的代码出现在任务 B 的 PR 里,reviewer 也没法从 diff 看出 agent 的真实意图。
  • 我们项目本来就把 feature 分支合进一个长期集成分支,怎么保护它? 集成分支按 main 待遇处理:加 protection、禁直接 commit、agent 也必须走 PR。

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