你打开 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 pushes 和 Restrict 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/squash | squash-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 rebase、git push --force、git 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 --hard、git 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 “没了”,它在一段时间内几乎肯定能恢复:
- 在本地找回旧 tip。 在任何还留着重写前那个 branch 的 clone 上,跑
git reflog show <branch>(或git reflog),抄下重写之前那个 SHA。然后git branch recovered-work <sha>并把它推到一个新 branch。 - 没有本地副本? 去 GitHub 上查这个 ref 的历史:branch 下的事件日志、PR 的 “force-pushed” timeline 条目(它会链到旧 SHA)、或者 repo 的 Activity 视图(Insights → Network / 该 branch 的 Activity)。直接访问
https://github.com/<owner>/<repo>/commit/<old-sha>往往还能解析出那个悬空 commit。 - forward-only 地恢复。 拿到旧 SHA 后,从它建一个新 commit 或新 branch。不要把恢复出来的状态再 force-push 盖回被毁的那个上——开一个干净的 PR,这样你不会第二次重写 history。
- 丢失的 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-pushhook 拦 non-fast-forward push。 - merge agent PR 前看
git log和 GitHub timeline。 - 撤销用
git revert,不用--amend或reset --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 没完成 patch
- Codex patch 和现有代码冲突
- Codex 解不了 merge conflict
- Codex PR 太大没法 merge
- Codex 跳过失败测试
- Codex 没遵守项目约定