Codex 自作主张改了 git history:amend / rebase / force-push 怎么禁掉

Codex 跑了 git commit --amend、交互 rebase 或 force-push,重写了共享分支的 history。用 AGENTS.md、sandbox network 开关和 GitHub ruleset 把 agent 锁死在 forward-only commit 上。

你打开 Codex 的 PR,diff 看着没问题,但 git log 不对:你最近三个 commit 没了,被一个你没写过的 “WIP” commit 替掉了。或者 Codex rebase 到一个老的 base 之后 force-push,同事的一个 commit 凭空消失。或者它 amend 了一个已经在 main 上的 commit,现在所有人 git pull 都报 “divergent branches”。

最快的修复(先做这个): 让 Codex 待在默认 sandbox 里,这样它没法悄悄连到 remote,再让 server 那边直接拒绝重写。在 ~/.codex/config.toml 里确认 sandbox_mode = "workspace-write"network_access = false(截至 2026 年 6 月这俩都是默认值)——这意味着任何 git push 都需要你明确批准,force-push 不可能在你背后发生。然后在 main 上加一个 GitHub ruleset,把 Block force pushesRestrict deletions 都打开。光这两步就能挡住灾难;下面的 AGENTS.md 规则是让 Codex 根本不去尝试

为什么老是这样?Codex 会 pattern match 它能看到的所有 git 指引——你的 README、老的 CONTRIBUTING.md、注释里贴的 Stack Overflow 片段。只要任何一处写着 “提前 squash 用 git commit --amend” 或 “push 前 rebase 到 main”,agent 就当成是项目政策照做,连共享分支也不放过。

真正稳的修复是三层:让 agent 真正读到的 “不许重写 history” 规则、Codex 自己的 sandbox 让 push 必须经过批准,加上 git server 那边无论如何都拒绝 force-push 的保护。

你属于哪一种?

症状大概的原因跳到
你做了很多 commit,PR 却只剩一个 WIP/squashsquash-on-push 默认值或 --amend 循环原因 1、4
Codex push 后同事的 commit 消失了rebase 到旧 base + force-push原因 2
review comment 链接 404;某个 commit SHA 变了agent amend 了已经 push 的 commit原因 3
未提交工作或本地 commit 没了,根本没 push脚本里的 git reset --hard / git clean原因 5
git pull 报 “divergent branches” / “non-fast-forward”任何已经到达 remote 的 history 重写恢复

常见原因

1. 老的 README 或 CONTRIBUTING.md 还在推荐 git commit --amend

几年前你写过 “push 前用 git commit --amend --no-edit squash fixup”。文档还在。Codex 读到了照做,amend 已经 push 过的 commit。

如何判断:grep -rin -E 'amend|rebase -i|force.push' README.md CONTRIBUTING.md docs/。如果 agent transcript 引用了这些行,源头就是它们。

2. Agent 用 rebase 来 “解决冲突”

Codex 看到 merge conflict,觉得 rebase 更干净。Rebase 完就 force-push。本地还 checkout 着旧分支的人现在 history 完全分叉。

如何判断: transcript 里搜 git rebasegit push --forcegit push -f。用 git reflog show <branch> 对比 before/after。

3. Agent amend 来 “修上一个 commit 的 typo”

Model 把 commit 当成草稿。它发现某个文件忘加,跑 git add forgotten.ts && git commit --amend --no-edit,然后 force-push。原 commit 的 SHA 没了,任何 review comment 的引用都 404。

如何判断:git reflog。看到 push 后面跟着一条 commit (amend) 就是它。

4. Agent harness 默认 squash-on-push

有些 Codex wrapper 默认 “把 task 的所有 commit squash 成一个再 push”。你本来想留中间 commit 给 review,现在都没了。

如何判断: transcript 里能看出多次独立编辑,PR 却只有一个 commit。检查你的 harness 配置里有没有 squash_commits: true(或类似的 key)。

5. Agent 跑了破坏性 cleanup 脚本

你的 scripts/cleanup.sh 里有 git reset --hard origin/main。agent 以为这是 “清状态”,跑下去把未提交工作和本地 commit 全擦了。

如何判断: transcript 包含 git reset --hardgit clean -fdx,或者调用了项目里会做这两件事的脚本。

最短修复路径

Step 1:在最近的 AGENTS.md 里写死禁令

Codex 读的是 AGENTS.md,不是老的 codex.md。它每个 session 拼一个指令块:把从 git root 往下到工作目录的每个 AGENTS.md,加上全局 ~/.codex/AGENTS.md,全部串起来。拼接顺序是 root 在前、leaf 在后,离工作目录最近的那个文件赢,因为它在拼好的 prompt 里出现得最晚(截至 2026 年 6 月的 Codex CLI)。所以把这些规则放在 repo 根目录的 AGENTS.md(如果某个子目录有自己的就放更近的那个),别埋在老 README 里:

## Git 规则(强制)

- 任何情况都不许跑 `git commit --amend`
- 不许跑 `git rebase``git rebase -i``git reset --hard`
- 不许跑 `git push --force``git push -f``git push --force-with-lease`
- 始终在当前 branch tip 上 append 新 commit。
- 出现 merge conflict 就停下来上报,不许用重写 history 的方式绕过。
- 要 "改" 前一个 commit,写一个新 commit。撤销用 `git revert`

拼起来的 AGENTS.md 上限是 32 KiB(用 project_doc_max_bytes 可调),所以规则写紧凑些。如果同一层有个过时文档老是盖过你,在那一层放个 AGENTS.override.md 就能悄悄把同级的 AGENTS.md 顶掉。

Step 2:清掉旧文档里的过时建议

找出所有重写 history 的相关条款:

grep -rn -E 'commit --amend|rebase -i|push --force|reset --hard' \
  README.md CONTRIBUTING.md docs/ .github/

要么删行,要么加 “DO NOT” 前缀并指向 AGENTS.md。对 agent 来说,老的正向建议比没建议更糟——它会当成当前指令来读。

Step 3:把 Codex 的 sandbox 收紧,让 push 必须批准

这是大多数人漏掉的原生控制。在默认的 workspace-write sandbox 里,网络访问是关闭的,所以任何需要联网的命令——包括 git push——在你批准之前都被拦着。检查 ~/.codex/config.toml

sandbox_mode = "workspace-write"   # 默认;不要切到 danger-full-access
approval_policy = "on-request"     # 或 "untrusted";千万别一刀切 "never"

[sandbox_workspace_write]
network_access = false             # 默认;push 没法悄悄连到 remote

保持这些默认值,Codex 在能 push 之前会停下来问你,你就能在 history 移动之前先看到那条命令。要避免的失败模式:用 --dangerously-bypass-approvals-and-sandbox 跑(或者 sandbox_mode = "danger-full-access" + approval_policy = "never"),那把所有护栏都拆了。full access 只在一次性的 VM 里用,绝不要在共享 repo 的 clone 上用。

Step 4:在 GitHub 上用 ruleset 保护分支

GitHub 现在的机制是 Rulesets(Settings → Rules → Rulesets → New branch ruleset)。老 repo 可能还在用 Settings → Branches → Branch protection rules;两个都行,而且新建一条 branch protection rule 默认就已经禁掉 force push 和删除了。对 main 和所有共享 release branch,打开:

  • Require a pull request before merging
  • Require status checks to pass
  • Restrict who can push to matching branches(最好谁都不能直接 push,只走 PR)
  • Block force pushes
  • Restrict deletions

这是保底。就算 Codex 想 force-push,server 也会用 remote rejected ... protected branch hook declined 拒掉。

也把 codex/*(如果你用 agent/* 就一起)加进 ruleset 的目标分支——Codex 推的就是这些 branch。两个并行 task 撞到同一个 branch,一 force-push 就会覆盖掉其中一个。

Step 5:用 pre-push hook 在 sandbox 层拦 non-fast-forward

有些 agent 配置允许在环境 setup 时装 client 端 hook。.codex/setup.sh(或你 harness 的 setup 脚本)里:

mkdir -p .git/hooks
cat > .git/hooks/pre-push <<'EOF'
#!/usr/bin/env bash
# 在 sandbox 层拒绝 force-push / amend-after-push
zero="0000000000000000000000000000000000000000"
while read -r local_ref local_sha remote_ref remote_sha; do
  if [[ "$remote_sha" != "$zero" ]]; then
    if ! git merge-base --is-ancestor "$remote_sha" "$local_sha"; then
      echo "ERROR: non-fast-forward push 已拦截,不要重写 history。"
      exit 1
    fi
  fi
done
EOF
chmod +x .git/hooks/pre-push

任何非 fast-forward push 在 sandbox 这层就被拒,根本到不了 GitHub。(铁了心用 git push --no-verify 能绕过它,所以 Step 3 和 Step 4 仍然重要。)

Step 6:merge 前看一眼 git log 和 timeline

PR review checklist 里加:

- [ ] `git log --oneline origin/main..HEAD` 显示的 commit 是我预期的
- [ ] 该 branch 的 `git reflog` 没有 `(amend)` 条目
- [ ] 该 branch 上没有 agent 之外的人的 commit
- [ ] PR 的 force-push 次数为 0(GitHub timeline 会显示 "force-pushed")

每个 PR 两秒,上面所有变种都能抓到。

已经被重写的 history 怎么找回来

如果 Codex 已经 force-push 了、某个 commit “没了”,它在一段时间内几乎肯定能恢复:

  1. 在本地找回旧 tip。 在任何还留着重写前那个 branch 的 clone 上,跑 git reflog show <branch>(或 git reflog),抄下重写之前那个 SHA。然后 git branch recovered-work <sha> 并把它推到一个新 branch。
  2. 没有本地副本? 去 GitHub 上查这个 ref 的历史:branch 下的事件日志、PR 的 “force-pushed” timeline 条目(它会链到旧 SHA)、或者 repo 的 Activity 视图(Insights → Network / 该 branch 的 Activity)。直接访问 https://github.com/<owner>/<repo>/commit/<old-sha> 往往还能解析出那个悬空 commit。
  3. forward-only 地恢复。 拿到旧 SHA 后,从它建一个新 commit 或新 branch。不要把恢复出来的状态再 force-push 盖回被毁的那个上——开一个干净的 PR,这样你不会第二次重写 history。
  4. 丢失的 commit 在 git 垃圾回收跑之前一直可达(本地 reflog 条目默认保留约 30 天)。早恢复别拖。

怎么确认修好了

  • 从一个测试 clone 跑 git push --force origin main 应该失败,报 protected branch hook declined(说明 ruleset 已生效)。
  • 在一个临时 repo 里让 Codex “amend 上一个 commit”,它应该拒绝或停下来上报,并引用 AGENTS.md 里的规则。
  • 在一个 Codex task 里触发一次 git push:它应该弹出批准提示,而不是悄悄推上去(说明网络 sandbox 在生效)。
  • 对最近几个 agent PR 跑 git reflog show <branch>,不应出现 (amend)rebase 条目。

预防

  • 把 “不许重写 history” 规则放在最近的 AGENTS.md(根目录或子目录),别放 README。
  • 每季度扫一遍 README/CONTRIBUTING 里过时的 git 建议。
  • main 加 GitHub ruleset,每个 codex/*agent/* branch pattern 也一起。
  • Codex sandbox 保持 workspace-write + network_access = false;共享 clone 上绝不用 danger-full-access
  • 装一个 pre-push hook 拦 non-fast-forward push。
  • merge agent PR 前看 git log 和 GitHub timeline。
  • 撤销用 git revert,不用 --amendreset --hard

FAQ

Codex 已经 force-push 了、commit 没了,能找回来吗? 通常能。在任何还留着旧 tip 的 clone 上跑 git reflog show <branch>,抓重写前那个 SHA,从它建 branch。如果没有 clone 留着,PR 在 GitHub 上的 “force-pushed” timeline 条目会链到旧 SHA,往往还能解析。见上面怎么找回来

GitHub ruleset 能挡住 Codex 在它自己的 feature branch 上重写 history 吗? 只有当你把那个 branch pattern 也设为目标才行。main 上的 ruleset 不保护 codex/feature-x。把 codex/*(和 agent/*)加进 ruleset,并行 task 就不会互相 force-push 覆盖。

我从没让它改,Codex 为什么会重写 history? 它从某处读到了一条指令——通常是老 README 或 CONTRIBUTING.md 里写着 “用 --amend squash” 或 “push 前 rebase”——然后当成项目政策。把那些文档审一遍(Step 2),并在 AGENTS.md 里放一条更响的规则(Step 1)。

光靠 AGENTS.md 规则能防住吗? 它是最大的那根杠杆,但属于软控制:是指令,不是硬拦截。配上 sandbox 网络设置(Step 3)和 server 端 ruleset(Step 4),这样一次失误也到不了共享 history。

让 agent 用 git push --force-with-lease 安全吗? 不安全。对一个清楚 remote 状态的人来说它比 --force 安全,但它仍然重写 history,而且 agent 可以满足那个 “lease” 检查照样把别人的工作冲掉。三个 force 变种都留在 AGENTS.md 的禁用清单里。

怎么撤销一个坏 commit 又不重写 history?git revert <sha>。它建一个新 commit 把坏的那个反转掉,原 SHA 保持不变,review 链接和同事的 clone 都还有效。对 merge commit,用 git revert -m 1 <merge-sha>

相关

外部参考:Codex sandboxing & approvals · GitHub rulesets

标签: #Codex #agent #排查 #git