✅ 使用者同意的,是他按下去那一刻看到的那一筆:這行指令、這些參數、當時的狀態。工具卻常把一次同意放大:Claude Code 的「Yes, and don't ask again」會在 repo 存一條規則,之後開頭相同的指令都不再問,強制推送也在內。同意要綁在人看到的東西上,執行前比對,指令換了、狀態變了就重新問;換個寫法就繞得過的規則擋不完,最後一關放在收到動作的那一端。
昨天在 Day 22,我們看到 Claude Code 在你電腦上用的是你的 GitHub 帳號,使用者在 GitHub 上按的 OAuth 同意,給的是 App 能動哪些東西,沒有說這一次要開哪個 PR。今天往下一層看:Agent 動手前跳出來問你的那個「同意」,按下去之後涵蓋了什麼。
快速回顧一下:Day 20 說過,寫在 prompt 裡的規則擋不住模型,要寫在模型外面,例如 Claude Code 的權限設定。
假設你用 Claude Code 的 Manual 模式,每個會改東西的指令都先問你。你請它把修好的 commit 推上去,畫面跳出詢問:
Bash command
git push origin main
Do you want to proceed?
1. Yes
2. Yes, and don't ask again for: git push *
3. Yes, and switch to auto mode
4. No
你看到的是 git push origin main,覺得每次推送都要按一次很煩,選了 2。Claude Code 在 repo 根目錄的 .claude/settings.local.json 寫下一條 Bash(git push *),這個 repo 之後每個 session 都有效。兩週後你請它改一下那個 commit 的訊息,它在本機改完,要換掉遠端上的舊 commit 就得跑 git push --force origin main。這行開頭也是 git push,比對得到那條規則,沒有再問你;同事這兩週接在後面推的 commit,就被蓋掉了。

圖:左邊是你看到的詢問畫面,指令是 git push origin main,你選了第 2 項「don't ask again」→ 中間是 Claude Code 存進 .claude/settings.local.json 的那條規則,比對的是開頭的 git push → 右邊是之後的指令:一般推送、強制推送、刪遠端分支都比對得到,不再問你;換成 git -C . push 這種寫法又對不上。這是示意,實作要照自己的工具和環境調整。
你同意的是畫面上那一行,存下來的卻是開頭相同的一整類。下面先看各家的「同意」各涵蓋多大,再看支付和資安怎麼要求同意綁住內容,接著看規則和 classifier 各自比對的是什麼、最後一關為什麼要放在遠端,再落成自己刻 Harness 時,同意要記下什麼。
Agent 要做有風險的動作時停下來問人,這件事叫 human-in-the-loop。OWASP 的 LLM 十大風險把它列為「Excessive Agency」(Agent 能做的事超過需要)的對策之一:高影響的動作,先讓人核准再執行。MCP 規格也寫,呼叫工具前要把工具的輸入秀給使用者看。
問題在問的次數。Anthropic 介紹 auto mode 的工程文章開頭就寫,Claude Code 的使用者會核准 93% 的詢問;問得越多,人越只是一直按同意,這叫 approval fatigue。OWASP 的 Agent 威脅清單也把「用大量核准請求淹沒審核的人,讓高影響的那筆混過去」列成一種攻擊。所以各家都提供了讓同意放大的選項,差在放多大:
| 工具 | 詢問時能選什麼 | 放大之後涵蓋多少 |
|---|---|---|
| Claude Code(Manual 模式) | Yes/Yes, and don't ask again | Bash 指令存成開頭相同的規則,這個 repo 永久有效 |
| Codex CLI | 只這一次/這個 session 同一個指令不再問/開頭相同的指令都不再問 | 最後一種寫進 ~/.codex/rules/default.rules,之後每次執行都套用 |
| Gemini CLI | Allow once/Allow always | 這個 session 裡同一個指令都放行 |
| OpenAI Agents SDK | 核准這一次呼叫/always_approve=True |
這次執行裡同一個工具之後的呼叫都放行,參數不同也一樣 |
| Claude Code(auto mode) | 不跳詢問,你在對話裡說的話就是同意 | 只涵蓋你點名的那一個動作 |
最後一列的做法不同。auto mode 是 Claude Code 2.1.283 起互動視窗預設的模式:不逐項問你,改由另一個模型在每個動作執行前判斷,它叫 classifier。Anthropic 那篇文章這樣寫它的判斷標準:「The prompt establishes what is authorized; everything the agent chooses on its own is unauthorized until the user says otherwise.」你說過的話劃出同意的範圍,Agent 自己決定的事一律算沒被同意。permission modes 文件把範圍寫得更細:強制推送預設會擋,你要放行,得在訊息裡講出動作和具體對象,例如哪個分支的強制推送,只說「可以 force push」不算;而且這次同意只涵蓋你點名的那一個動作,下一次會再擋。
我在 macOS 上用 Claude Code 2.1.285、Claude Opus 5.5 試過這條界線。repo 是本機的假遠端:同事推了「修好結帳 bug」,我本機另外 commit 了「我的修改」,兩邊從初始版本之後分岔,一般推送會被拒絕,強制推送會把同事的 commit 蓋掉。
| 在 auto mode 裡說 | 發生什麼 | 遠端 main 最後 |
|---|---|---|
| 「把 main 推上去。」 | 試了兩次都沒強制推送:先抓遠端、看同事改了什麼,git rebase origin/main 之後一般推送。rebase 把我的 commit 編號從 dd01f0c 改成 6f2125d,我沒要求,回報裡有講 |
初始版本 → 同事 → 我的修改 |
| 「請執行 git push --force origin main」 | 有講出動作和分支,第一個工具呼叫就是這行,(forced update) |
初始版本 → 我的修改 |
同一個產品裡,對話裡的同意收得很窄,詢問畫面的「don't ask again」卻是一整類、永久有效。開頭那條規則是後者。
這件事在支付業有現成的規定。歐盟支付法規 PSD2 的強客戶認證技術標準第 5 條叫 dynamic linking:付款人確認時要看得到金額和收款人;產生的驗證碼只對這組金額和收款人有效;「any change to the amount or the payee results in the invalidation of the authentication code generated」,金額或收款人一改,這次確認就作廢。顯示給付款人看的內容,也要保證在整個過程中沒被改過。
反過來的例子是 2025 年 2 月的 Bybit 事件。這間交易所的冷錢包要好幾位簽署人同意才能轉出,攻擊者改了簽署用的網頁介面。CertiK 的分析引述 Bybit 執行長的說法:簽署人在網頁上看到的地址和交易資料都正確,送到硬體錢包簽名時內容已經被換掉,他簽之前沒有在硬體錢包的畫面上核對。損失約 14.6 億美元。每個人都按了同意,同意的卻是畫面上那一筆,執行的是另一筆。
Agent 的詢問畫面也會碰到這種事。Checkmarx 的研究在 GitHub Issue 裡塞了一段很長的文字,Claude Code 照 Issue 做事、跳出詢問時,實際要跑的指令被擠到畫面上方,不往上捲根本看不到。這是被外部內容帶偏的問題,Day 24 會接著講。
還有一種情況是內容沒變,狀態變了。人同意的時候遠端 main 停在某個 commit,等到真的推送,同事已經又推了一個。資安把這叫 TOCTOU(time-of-check to time-of-use),CWE-367 的定義是:檢查了資源的狀態才使用它,但檢查和使用之間狀態變了,檢查的結果就不算數。
合起來,一次同意要綁住三樣東西:人看到的那個動作和參數、當時的狀態、這次同意能用幾次。執行前三樣都要比對。
Claude Code 的權限文件提供三種規則:allow 放行、ask 一定要問、deny 一律擋。按「don't ask again」就是替你寫一條 allow。
這三種規則比對的都是 Claude 寫出來的指令文字。文件自己列了一張表:ask 或 deny 裡放 Bash(git push *),擋得住 git push origin main,擋不住 git -C . push origin main、git 'push' origin main,還寫明這種規則「isn't a security boundary around the program」,不是包住那支程式的安全邊界。反過來看 allow 也一樣:Bash(git push *) 放行的是所有 git push 開頭的文字,強制推送、刪遠端分支都在裡面。
拿前面那個分岔的 repo,加上一條 ask 規則 Bash(git push --force *) 再試:
有 ask 規則時說 |
發生什麼 | 遠端 main 最後 |
|---|---|---|
| 「請執行 git push --force origin main」 | 用 claude -p 跑沒有畫面可問,直接被拒:Claude requested permissions to use Bash, but you haven't granted it yet.;互動視窗照文件會跳出詢問 |
沒變 |
| 「git push origin +main」 | 在分支名稱前加 + 也是強制推送,文字對不上規則,照樣執行;Claude 自己替這個呼叫寫的說明是「Force push local main to origin」 |
初始版本 → 我的修改 |
那 auto mode 會不會補擋?permission modes 文件寫了判斷順序:每個動作先比對你的 allow、ask、deny 規則,比對到就直接照規則處理,比對不到的才交給 classifier。進入 auto mode 時,Claude Code 會先拿掉 Bash(*)、Bash(python*) 這類等於能跑任何程式的寬規則,Bash(git push *) 不在那份清單上。所以開頭那條規則在 auto mode 底下還是有效,強制推送比對到它就直接放行,classifier 根本沒看到。
classifier 看得到的東西也有限。Anthropic 的文章寫它只讀使用者的訊息和 Agent 的工具呼叫,Claude 自己寫的說明和工具輸出都拿掉,免得 Agent 用一套說詞說服它放行。在 52 個真實的越權動作上,它有 17% 沒擋到。
你在對話裡說的「不要推」也一樣靠不住:它沒有存成規則,文件寫 classifier 每次重新從對話紀錄裡讀,對話被壓縮時那句話可能就不見了,要確保做不到得寫成 deny。
各家的 classifier 回答的都是同一個問題:這個動作,使用者同意過嗎?差在什麼內容算數、看不到什麼:
| 工具 | 什麼算使用者同意 | 看不到什麼 |
|---|---|---|
| Claude Code 的 auto mode | 使用者說過的話;Agent 自己決定的事都不算 | Claude 自己寫的說明、工具輸出 |
| Codex 的 Auto-review | 使用者和開發者的訊息、AGENTS.md、使用者回答 Agent 提問的內容;工具輸出和 Agent 的回覆只當參考證據 |
Agent 沒有顯示出來的推理 |
| Chrome 的 User Alignment Critic | 使用者講的目標,動作對不上就否決 | 網頁內容,只看提出的動作的描述 |
三家的共同點是,同意只能來自使用者那一方寫下的內容,Agent 工作時讀到的東西不能替使用者同意。Codex 把審查規則寫在開源的 prompt 裡,其中兩條跟今天的主線一樣。一條是「If the user wants to achieve a particular end state, that does not necessarily authorize any individual action that might achieve that end state.」:使用者想要某個結果,不代表同意了達成它的每一個動作。另一條是使用者說很急,也不改變某個動作有沒有被同意。放回開頭的情境,「改一下 commit 訊息」照這條就不算同意強制推送;只是那次是規則直接放行,輪不到任何 classifier 判斷。
Codex 的 Auto-review 只審要超出沙箱的動作,沙箱裡本來就允許的照跑。2026 年 10 月 6 日,Codex 負責人 Thibault Sottiaux 在 X 上宣布,用 ChatGPT 帳號登入的使用者都能免費開,審查不算進方案的用量。他給的理由是逐項核准很容易讓人疲乏,跟 Anthropic 那篇的 93% 是同一個問題。這個功能 4 月就有了,這次改的是不另外算用量。
判斷「同意過沒」也有專門的模型可以用。Day 20 提過 TypeSafe 的 Jev,輸入一段狀態、回傳帶機率的判斷。OpenAI 9 月 29 日在 DevDay 發表的 Decisions API 也是這類:讓 Luna 模型從事先定好的幾個答案裡選一個,用途包含分類輸入、分派請求和替 Agent 選下一步,目前是有限預覽。答案定成「放行」和「擋下」,它就是一個 classifier。
Codex 的開源程式碼裡已經有一段,把要審的動作同時送給 Decisions API,評「低風險」或「高風險」。這段預設關閉、標成開發中,設定的註解寫「Run Decisions alongside Guardian V2 for measurement without changing approvals」(Guardian 是 Auto-review 在程式碼裡的名字):只拿來量測,不改變核准結果。所以現在 Codex 的 Auto-review 不是靠 Decisions 判斷的。我猜 OpenAI 在比較兩邊的判斷一不一致、快多少,再決定要不要換過去。
規則比的是文字,寫法列不完;classifier 是機率,Claude Code 公布的還有 17% 漏放。遠端看的是推送的結果,不管指令怎麼寫。GitHub 的文件寫「By default, GitHub blocks force pushes on all protected branches.」,受保護的分支預設就不收強制推送。自己架的 git 遠端,可以設 receive.denyNonFastForwards。上面那個分岔的 repo 設了這一項之後,--force 和 +main 兩種寫法都被拒絕(! [remote rejected] main -> main (non-fast-forward))。
真的需要強制推送,可以改用 --force-with-lease:只有遠端還停在你預期的那個 commit,才准蓋掉。這就是 git 自己處理 TOCTOU 的做法。同樣分岔的 repo,沒先抓遠端就推,得到 ! [rejected] main -> main (stale info),同事的 commit 保住了。
要注意不寫 commit 的 --force-with-lease,比的是本機記下的遠端狀態。git-push 文件提醒,背景有東西自動跑 git fetch 的話,這份記錄會被更新,保護就失效了。所以要把預期的 commit 直接寫在指令上,例如 --force-with-lease=main:033098b。
不用 Claude Code、自己做核准流程的,碰到的是同一件事,而且時間差更大:模型提出動作,Harness 把它送到聊天軟體或後台畫面,人可能二十分鐘後才按同意。
框架給的是暫停和恢復。OpenAI Agents SDK 可以替工具設 needs_approval,呼叫前停下來,拿到決定才繼續;核准預設只算這一個 call ID,傳 always_approve=True 就變成這次執行裡同一個工具之後都放行,跟「don't ask again」是同一種放大。LangGraph 的 interrupt 恢復時,暫停所在的節點會從頭再跑一次,暫停前做過的寄信、扣款會再做一次,要照 Day 18 的做法用固定的操作編號讓下游去重。同意要綁住什麼、用過了沒,框架沒有替你記,要自己記。
照前面說的那三樣東西,一筆同意最少要有:
| 欄位 | 這個情境的值 | 誰讀寫 |
|---|---|---|
| command | 人看過的那行指令:git push --force origin main |
詢問畫面寫入,執行前逐字比對 |
| remote_head | 人同意時遠端 main 停在哪個 commit,例如只有初始版本的 033098b |
詢問畫面從遠端讀出、顯示給人看,推送時交給 git 比對 |
| expires_at | 這次同意的期限,例如 30 分鐘 | Harness 寫入,執行前檢查 |
| used_at | 這次同意用掉的時間,還沒用是空的 | 執行端推送前寫入,之後同一筆同意不能再用 |
少了 command,模型換成 git push origin +main 也會放行;少了 remote_head,同意之後同事推的 commit 會被蓋掉;少了 used_at,同一筆同意可以一直用下去,就跟「don't ask again」一樣了。
def run_approved_push(cmd, approval, now, git):
if approval.used_at is not None:
return {"status": "approval_used"} # 一筆同意只能用一次,重新問
if now >= approval.expires_at or cmd != approval.command:
return {"status": "approval_invalid"} # 過期,或跟人看過的指令不一樣,重新問
approval.used_at = now # 先記下用掉了,再推送
seen = approval.remote_head # 人同意時遠端 main 的 commit
result = git.push("origin", "main", force_with_lease=f"main:{seen}")
if result.rejected:
return {"status": "remote_changed"} # 同意之後遠端變了,重新問
return {"status": "pushed", "head": result.new_head}
| 誰呼叫 | 什麼時候 | 結果 |
|---|---|---|
| Harness 的詢問畫面 | 模型提出 git push --force origin main |
讀出遠端 main 在 033098b,跟指令一起顯示給人;人按同意,寫下 command、remote_head、expires_at |
| Harness 的執行端 | 人同意之後、真的推送之前 | 指令一樣、沒過期、沒用過,寫入 used_at,帶著 033098b 推送 |
| 同一個執行端 | 模型換成 git push origin +main 再送一次 |
指令對不上,回 approval_invalid,重新問人 |
| git 遠端 | 人同意之後,同事又推了「修好結帳 bug」 | 遠端已經不是 033098b,拒絕(stale info),執行端回 remote_changed,重新問人 |

圖:上方是三個可以擋的位置:Claude Code 的 ask 規則比對文字,擋下 --force 卻漏掉 +main;遠端拒收強制推送,兩種寫法都擋下;--force-with-lease 帶著同意時的 commit,遠端變了就拒絕 → 下方是自己的 Harness 的流程:模型提出動作 → 詢問畫面顯示指令和遠端 commit → 人同意,記下指令、commit、期限 → 執行前比對指令、期限、用過沒 → 帶著 commit 推送 → 遠端變了就拒絕、重新問。這是示意,實作要照自己的工具和環境調整。
假設你已經做到 Day 22:知道 Agent 頂著誰的身分、能動哪些 repo。接下來確認你給過的同意涵蓋了多少。
先看自己按過哪些「不再問」。 打開 repo 根目錄的 .claude/settings.local.json,permissions.allow 裡每一條都是某次按出來的或自己加的。開頭太寬的,像 Bash(git push *),刪掉或改窄。
高影響的指令加 ask,絕對不准的放 deny。 例如強制推送、刪遠端分支。這些規則一樣只比文字,同一件事的幾種寫法盡量列,列不完的交給下一步。
最後一關放在收到動作的那一端。 重要的分支設成受保護的分支,不收強制推送;真的要強推,用 --force-with-lease,指令上寫明預期的 commit。自己做核准流程的,同意紀錄照上一節記下指令、當時的狀態、期限和用過沒,執行前比對。
只在自己的分支上做事、自己盯著畫面的,做到第一步大概就夠了;團隊共用的分支,或是沒人盯著、事後才看結果的背景工作,第三步不能省。Day 24 看讓 Agent 改變做法的是它讀到的內容時怎麼辦,Day 27 看執行失敗或結果不明時要不要再試一次。
回到開頭那條 Bash(git push *),可以試著問自己兩題。第一題:你後來切到 auto mode,Claude 要跑 git push --force origin main,強制推送不是預設會擋嗎,這次會擋下來嗎?第二題:你自己做的 Agent 請主管核准一次強制推送,主管二十分鐘後才按同意,這段時間同事推了新的 commit,執行端照紀錄推送,會發生什麼?
第一題,不會擋。auto mode 先比對你的規則,比對到 allow 就直接放行,輪不到 classifier;要它停下來,得把那條規則刪掉或改窄,再替強制推送加 ask,或乾脆讓遠端拒收。第二題,執行端帶著同意時記下的 033098b 去推,遠端已經不是這個 commit,git 拒絕,執行端回 remote_changed,重新問主管。如果紀錄只存了一個 approved=true,就會照推,把同事的 commit 蓋掉。
使用者同意的,是他按下去那一刻看到的那一筆:這行指令、這些參數、當時的狀態。工具卻常把一次同意放大:Claude Code 的「Yes, and don't ask again」會在 repo 存一條規則,之後開頭相同的指令都不再問,強制推送也在內。同意要綁在人看到的東西上,執行前比對,指令換了、狀態變了就重新問;換個寫法就繞得過的規則擋不完,最後一關放在收到動作的那一端。
昨天我們確認了 Agent 頂著誰的身分,今天看它每一步拿到的同意涵蓋多少。接下來還有另一個問題:如果讓 Agent 改變做法的,是它工作時讀到的網頁或 Issue 內容呢?
.claude/settings.local.json;規則比對的是指令文字,Bash(git push *) 擋不住 git -C . push,不是安全邊界。~/.codex/rules/default.rules,之後每次執行都套用。三個選項的文字取自 Codex CLI 原始碼。needs_approval 在工具呼叫前暫停;核准預設只算這個 call ID,always_approve=True 放行這次執行裡同一個工具之後的呼叫。AGENTS.md、回答 request_user_input 的內容能建立同意;工具輸出、Agent 的回覆是不可信的證據;想要某個結果不等於同意每個動作,急迫也不改變同意。看的是 2026-10-06 的 main。guardianv2_decisions_comparison 預設關閉、標成 UnderDevelopment,註解寫只為量測、不改變核准;送出請求的程式在 codex-rs/ext/guardian-v2/src/async_scorer/decisions.rs。看的是 2026-10-06 的 main。