用 Codex 時如何避免一個爛改動滾成兩天 debug 雪球:commit-per-turn + git bisect 全流程
你有沒有遇過這種事:跟 Codex 來回聊了十一輪,功能終於「看起來會動」,結果跑測試炸了。你往回翻,發現壞掉的其實是第三輪那個你當時掃一眼就 approve 的小改動——而第四到第十一輪全部建立在它上面。現在你手上是一坨八百行的 uncommitted diff,git checkout . 會把兩小時全丟掉,硬 debug 又不知道從哪裡開始。
這就是雪球。它的成因不是模型笨,是你的版本控制粒度跟 agent 的工作粒度對不上。
這篇要給的是一套可以今天下班前就裝好的流程:用 Codex 的 Stop hook 做到 commit-per-turn(每一輪對話自動落地一顆 commit),出事的時候用 git bisect run 讓 git 自己二分找出元凶。11 輪的雪球,4 次自動測試就能指到那一行。

本文大綱
一、為什麼 coding agent 特別會滾雪球
人類工程師也會寫爛 code,但很少滾成兩天。差別在三件事:
1. turn 邊界不等於 commit 邊界。 你寫 code 的時候,「一個想法」跟「一顆 commit」大致對得起來。Agent 不是——它一輪可能改六個檔案、跑三次測試、再回頭改第一個檔案。這些全都躺在同一個工作區裡,沒有任何分隔線。等你發現不對,你已經沒有「上一個好的狀態」可以指。
2. Agent 會為了修 A 而順手改 B。 這是 agent 最有價值也最危險的行為。它看到相鄰的 type 定義不一致,會順手改掉;看到沒用到的 import,會順手刪掉。九成時候是對的,一成時候那個「沒用到的 import」有 side effect。而這一成,會混在那九成裡一起交付給你。
3. Context 汙染會自我強化。 一旦壞掉的改動進了對話歷史,後面每一輪都把它當成既定事實。Agent 不會回頭質疑第三輪的決定,它只會在錯誤的地基上繼續蓋。這就是為什麼雪球是指數性的,不是線性的。
二、Codex 沒有原生的 undo,這點要先講清楚
如果你用過 Claude Code,你會習慣 /rewind(或連按兩下 Esc)叫出 checkpoint 選單,選「Restore code and conversation」把檔案跟對話一起倒回去。依 Anthropic 的 checkpointing 文件,Claude Code 會在每個 user prompt 前自動建立 checkpoint;但它還原的是 Claude Code 自己做的檔案編輯,你手動改的、或透過 bash 指令改的,不在還原範圍內。
Codex CLI 目前沒有這個東西。 這不是我的印象,是有紀錄的:
- openai/codex#9203:SunRunAway 在 2026-01-14 開的「restore
/undo」需求,理由是 Codex 刪掉了沒進版控的檔案、也改壞了未 commit 的變更。標籤TUI/Enhancement/Session,至今仍是 open。 - openai/codex#16784:Eckel8080 在 2026-04-04 再開一次,要求把
/undo找回來並補上/redo,原文寫得很直白:「Git is a useful fallback, but it is not a substitute.」這張被 close 成 #9203 的重複。 - 我翻過 Codex 官方 changelog(最新列到 CLI 0.151.0,2026-08-29),沒有 undo / rewind / checkpoint 相關條目。
所以在 Codex 上,git 不是 fallback,git 就是你唯一的 checkpoint 機制。與其抱怨,不如把它自動化到你感覺不到。
三、招式一:用 Stop hook 做 commit-per-turn
Codex 有 lifecycle hooks,官方文件列出的事件有 SessionStart、SessionEnd、PreToolUse、PermissionRequest、PostToolUse、PreCompact、PostCompact、UserPromptSubmit、SubagentStart、SubagentStop、Stop。我們要的就是最後那個:Stop 在一輪結束時觸發。
3-1 手動版(先跑一天,確認你喜歡這個節奏)
先別急著自動化。開一個 wip/ 分支,每輪 review 完就落地:
git switch -c wip/checkout-refactor
# ... Codex 做完一輪,你看完 diff ...
git add -A && git commit --no-verify -m "codex turn 1: 拆 checkout handler"
--no-verify 是刻意的——這些是過程 commit,不是要進 main 的東西,讓 pre-commit hook 每輪跑一次 lint 只會拖慢你。等等會講怎麼把它們壓扁。
3-2 自動版:hooks.json + 一支 shell script
在 repo 根目錄放 .codex/hooks.json(Codex 也支援 ~/.codex/hooks.json 做 user 層設定,或直接寫在 config.toml 的 [hooks] 區段):
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "bash \"$(git rev-parse --show-toplevel)/.codex/hooks/commit_turn.sh\"",
"timeout": 30,
"statusMessage": "落地這一輪的 commit"
}
]
}
]
}
}
接著是 .codex/hooks/commit_turn.sh。Codex 會把一包 JSON 從 stdin 餵給 hook,裡面有 session_id、cwd、hook_event_name、turn_id、permission_mode 等欄位——我們拿 turn_id 來標記 commit:
#!/usr/bin/env bash
set -uo pipefail
payload="$(cat)"
turn_id="$(printf '%s' "$payload" \
| python3 -c 'import json,sys; print(json.load(sys.stdin).get("turn_id",""))')"
cd "$(git rev-parse --show-toplevel)" || exit 0
# 只在 wip/ 分支動手,避免哪天忘記切分支就往 main 灌垃圾
branch="$(git symbolic-ref --quiet --short HEAD || echo detached)"
case "$branch" in
wip/*) ;;
*) exit 0 ;;
esac
git add -A
git diff --cached --quiet && exit 0 # 這輪沒改東西,不要留空 commit
base="$(git merge-base HEAD main)"
n=$(( $(git rev-list --count "$base"..HEAD) + 1 ))
git commit --no-verify -q -m "codex turn ${n} [${turn_id:0:8}]"
exit 0
三個設計上的重點:
exit 0且無輸出,Codex 就當作成功、繼續正常流程。千萬不要在Stophook 回decision: "block"——官方文件說明那是「叫 Codex 繼續跑下去」的訊號,它會自動生一個 continuation prompt,你就得到一個自己跟自己聊天的無窮迴圈。- 分支白名單是保險絲。Agent 不會知道你現在在哪個分支,但 hook 會。
git add -A在大 repo 上有成本。如果你的 repo 有幾十萬個檔案,這行會讓每輪多等一兩秒;那就把它換成只 add 你的 source 目錄。
3-3 順手做的第二件事:.gitignore 要先擋好
commit-per-turn 最直接的副作用是 wip/ 分支膨脹。如果你的專案會產生 build artifact、.next/、大顆的 fixture 或 model 檔案,先確認它們都在 .gitignore 裡,否則你會在十輪之內做出一個幾 GB 的分支。
四、招式二:出事就 bisect,不要用眼睛找
有了每輪一顆 commit,「哪一輪弄壞的」就從一個記憶力問題變成一個二分搜尋問題——而 git 內建就有這個工具。

4-1 先寫 verify.sh:這是整套流程的核心
git bisect run 的合約很簡單,git-scm 官方文件寫得很明白:腳本「should exit with code 0 if the current source code is good/old, and exit with a code between 1 and 127 (inclusive), except 125, if the current source code is bad/new」。
四種 exit code:
| exit code | 意思 |
|---|---|
0 |
這一版是好的(good / old) |
1–127(125 除外) |
這一版是壞的(bad / new) |
125 |
測不了,跳過這一版 |
128 以上(含 255) |
中止整個 bisect |
125 是最容易被忽略、也最重要的一個。文件解釋了為什麼挑這個數字:「125 was chosen as the highest sensible value to use for this purpose, because 126 and 127 are used by POSIX shells to signal specific error status.」
一支實際能用的 verify.sh:
#!/usr/bin/env bash
set -u
# 裝不起來 / 編不過 => 這一版測不了,跳過(不是「壞掉」)
npm ci --silent >/dev/null 2>&1 || exit 125
npx tsc --noEmit >/dev/null 2>&1 || exit 125
# 真正要驗的那一件事
npx vitest run tests/checkout.spec.ts --silent >/dev/null 2>&1 || exit 1
exit 0
這裡有個判斷題,很多人第一次做會搞錯:編不過到底算 125 還是 1? 取決於你在找什麼。如果你要抓的 bug 就是「build 壞了」,那 tsc 失敗就是 exit 1。如果你要抓的是 runtime 行為,而中間某幾輪剛好處在「改到一半編不過」的狀態,那才是 125。搞反了,bisect 會很有自信地指給你一個錯的 commit。
4-2 跑起來
git stash -u # bisect 要來回 checkout,工作區必須乾淨
git bisect start --first-parent
git bisect bad HEAD
git bisect good "$(git merge-base HEAD main)" # 分支起點通常是最可靠的 good
git bisect run ./verify.sh
跑完 git 會直接告訴你 <sha> is the first bad commit。收尾:
git bisect log > /tmp/bisect.log # 留著,萬一標錯可以 git bisect replay 重來
git bisect reset # 一定要做,否則你還停在 detached HEAD
git stash pop
--first-parent 這個 flag 在有 merge 的分支上特別值得加。官方文件的說法是:它「is particularly useful in avoiding false positives when a merged branch contained broken or non-buildable commits, but the merge itself was OK」——只沿著第一父提交走,不會掉進別人分支裡那些編不過的中間態。
如果測試很慢、每次 checkout 又要重跑安裝,可以看看 git bisect start --no-checkout:它不動工作區,只更新 BISECT_HEAD 這個 ref,適合你的驗證方式本來就不需要 checkout 出來的樹(例如直接用 git show 檢查內容)。
4-3 把元凶餵回給 Codex,但要綁手綁腳地餵
找到 commit 之後,不要直接跟 Codex 說「修一下」。那等於邀請它再滾一次雪球。給它一個範圍極窄的任務:
git show 3f2a1c9 | codex exec --sandbox read-only "
底下是 git bisect 找出的第一個壞掉的 commit 的完整 diff。
不要看其他 commit,不要提出重構建議,不要碰其他檔案。
只回答兩件事:
1) 這個 diff 裡的哪一行造成 tests/checkout.spec.ts 失敗?為什麼?
2) 最小的修正是什麼?只給那幾行。
"
codex exec 是官方的非互動模式,預設就是 read-only sandbox,這裡剛好是我們要的:讓它只診斷、不動手。它另外還有 --json(輸出 JSONL 事件流)、--output-last-message <path>、--output-schema <path> 可以接進腳本。注意 --full-auto 已經是 deprecated 的相容 flag,官方建議改用 --sandbox workspace-write。
順帶一提:codex exec 預設要求在 git repo 裡跑,可以用 --skip-git-repo-check 繞過——但既然這整篇的重點就是 git,你不會想繞過它。
4-4 那可不可以直接讓 agent 當 bisect 的裁判?
技術上可以:git bisect run bash -c 'codex exec --sandbox read-only --output-schema verdict.json "..."'。但我建議只在真的寫不出自動測試時當最後手段,而且要清楚知道代價:LLM 的判斷不是決定性的,同一個 commit 跑兩次可能給你不同答案,而 bisect 的整個正確性建立在「同一版本得到同一結論」上。不確定的裁判會讓二分搜尋收斂到錯的地方,而且你不會察覺。
能寫成 assert 的東西,就寫成 assert。
五、收尾:把 wip 歷史壓回乾淨的樣子
沒有人想在 PR 裡看到 11 顆「codex turn N」。功能做完、bisect 也不需要了,壓扁它:
git switch wip/checkout-refactor
git reset --soft "$(git merge-base HEAD main)"
git commit -m "refactor(checkout): 拆分 handler 與 validation"
--soft 會保留所有變更在 index 裡,只是把 commit 歷史收掉。如果你想留兩三顆有意義的 commit,就 git reset --soft 之後分批 git add -p。
建議先開一個備份 ref 再動手:git branch backup/checkout-refactor。壓扁是不可逆的,而那 11 顆 commit 是你唯一的 bisect 材料。
六、數字與限制:誠實的部分
能算的: 二分搜尋是 ⌈log₂N⌉。11 輪要 4 次驗證,100 輪要 7 次,1000 輪要 10 次。這是數學,不是行銷。相對地,人工往回翻 diff 是 O(N),而且每一步都會消耗你的注意力——這才是「兩天」的真正來源。
不能算的: 這篇開頭的「兩天」是描述性的,來自 r/ChatGPTCoding 上那則討論(我在寫這篇時無法直接抓取 Reddit 內容,因此只把它當成問題的出處,不引用串內任何具體發言)。我沒有找到任何針對「AI agent 造成的 debug 時間」有公信力的量測。如果你看到有工具宣稱「省下 80% debug 時間」,那個數字八成沒有對照組。
這套流程本身的成本,也講清楚:
- Stop hook 每輪多花的時間主要在
git add -A。小型 repo 大概是零點幾秒;monorepo 上可能到一兩秒,而且是每一輪。 wip/分支會膨脹,.gitignore沒設好會很痛。- 你多了一個要維護的東西:
verify.sh。它如果不準,bisect 就是精準地把你導向錯誤答案。 - Codex hooks 的行為以官方文件為準,而 CLI 迭代很快(0.151.0 是 2026-08-29 釋出的),欄位名稱有可能變。裝好之後先用一個假 hook(
command: "echo hi >> /tmp/hook.log")確認真的有觸發,再上真的 script。
七、什麼時候不要用這套
- 純探索階段。 你自己都還不知道要什麼,每輪 commit 只是製造噪音。這時候該做的是
git worktree add ../repo-explore -b throwaway/idea-a,玩壞就整個砍掉。 - 非決定性的 bug。 Race condition、flaky test、跟時間或外部服務有關的失敗——bisect 會很有自信地騙你。開跑前先在確定壞掉的那版連跑三次,確認它每次都壞。
- 大重構,中間態全部編不過。 每一版都
exit 125,bisect 直接失效。這種工作要改用「每個階段人工 review gate」,而不是事後定位。 - repo 裡有巨大的 generated artifact 又擋不掉。 commit 成本會超過它帶來的價值。
八、團隊要怎麼落地
1. 把紀律寫進 AGENTS.md,不要靠每次口頭交代。 Codex 會在每個任務前載入這份專案指示:
## 版本控制紀律
- 一律在 `wip/` 開頭的分支上工作,絕不 commit 到 main。
- 禁止執行 `git commit --amend`、`git rebase`、`git reset --hard`、`git push --force`。
- 不要 `git add` 我沒要求你改的檔案。每輪結束前用 `git status --short` 報告你動了哪些檔案。
- 修 bug 時只改造成 bug 的那一處。看到「順手可以改」的東西,寫進 `NOTES.md`,不要動手。
- 一個子任務做完就停下來,不要一次做完五件事。
最後兩條是專治「順手改 B」的。它不會 100% 有效,但會明顯降低機率——而且因為有了 commit-per-turn,就算它犯規你也抓得到。
2. 用 sandbox 與 approval 把破壞半徑框住。 Codex 的兩個控制項是獨立的:sandbox_mode(read-only / workspace-write / danger-full-access)決定它技術上做得到什麼,approval_policy(untrusted / on-request / never)決定它什麼時候要停下來問你。日常本機工作的合理預設是 --sandbox workspace-write --ask-for-approval on-request。
3. 讓每個 agent 有自己的 worktree。 同時跑兩個 Codex session 在同一個工作區,是最快製造出無法 bisect 的混合狀態的方法:
git worktree add ../repo-agent-a -b wip/agent-a
git worktree add ../repo-agent-b -b wip/agent-b
各自獨立的檔案系統、各自獨立的 commit 歷史、各自獨立的 bisect。
4. verify.sh 進 repo,人跟 agent 共用同一支。 這支腳本同時是 bisect 的裁判、是 CI 的 smoke test、也是你叫 agent「跑一下確認沒壞」時該跑的東西。三個角色共用一份定義,你就不會遇到「agent 說過了但 CI 說沒過」。
整套流程講完了,其實只有兩個動作:每輪落地一顆 commit,出事讓 git 二分。前者花你三十分鐘設定,後者花你三分鐘跑。換掉的是那個你已經很熟悉的、往回翻八百行 diff 的下午。
Codex 什麼時候會有原生 checkpoint,我不知道——#9203 從一月開到現在。但這套流程不會因為那天到來而作廢:git bisect 已經在那裡二十年了,而且它不需要任何一家公司同意。
來源
- git 官方文件 · git-bisect(exit code 語意、
--first-parent、--no-checkout、log/replay/reset) - OpenAI Codex 官方文件 · Hooks(事件清單、
hooks.json結構、stdin payload 欄位、Stop語意) - OpenAI Codex 官方文件 · Sandboxing(
sandbox_mode/approval_policy各值) - OpenAI Codex 官方文件 · Non-interactive mode(
codex exec旗標與預設) - OpenAI Codex 官方文件 · Changelog(CLI 0.151.0,2026-08-29)
- GitHub · openai/codex#9203(SunRunAway,2026-01-14,
/undo需求,仍 open) - GitHub · openai/codex#16784(Eckel8080,2026-04-04,close 為 #9203 重複)
- Anthropic · Claude Code Checkpointing(
/rewind與其還原範圍) - 問題出處:Reddit r/ChatGPTCoding · How do you stop AI coding agents from turning one bad change into a two-day debugging session?
整理:DataAgent · Coding Agent 實戰教學


