把 AI 程式碼審閱放進 Git 的 pre-push Hook,乍看很合理:程式碼要推送到遠端前,先確認有人或工具檢查過;沒審閱或發現重大問題,就先擋下來。

我用 OpenQodex 0.11.0 做了幾輪隔離測試。結果沒那麼簡單。ShellCheck 確實抓到了我刻意埋在 Shell 腳本裡的錯誤,Git Hook 也真的可以阻止未審閱的推送。然而,獨立 AI Reviewer 沒有完成審閱時,Hook 不見得會阻止 git push。

這不代表 OpenQodex 的 AI 審閱品質不好。那一部分根本沒通過驗收。 真正需要拆開看的是三件事:靜態掃描、獨立 AI Reviewer,以及根據既有審閱收據做決策的推送門禁。它們不是同一個自動執行的步驟。

ShellCheck 抓得到問題,但前提是掃描器真的有執行

我準備了一個可隨時刪除的 Git 測試倉庫,放進同一個 Shell 腳本的兩個版本。基準版替變數加上雙引號;修改版刻意拿掉引號,製造 ShellCheck 的 SC2086 問題。

這一輪固定使用 OpenQodex 0.11.0,只執行掃描指令,沒有呼叫任何模型。核心參數如下:

openqodex scan --uncommitted --only shellcheck --block-on-severity nitpick

為了易讀,這裡省略了實際測試腳本中的 --cwd、--report-dir 與已驗證套件的固定路徑;不代表實驗逐字執行了上面這一行。若要重現,應先在隔離倉庫準備真正的 Git diff,再使用固定版本的 CLI。參數語意可參考固定版本的官方文件。

同一份人工製造的修改,結果卻因為掃描器是否可用而不同:

測試條件ShellCheck 狀態最終 finding掃描退出碼
離線且禁止安裝not_installed00
允許安裝並實際執行ran1(SC2086)1

這是 2026 年 10 月 10 日固定版本合成測試的去識別化觀察結果。在允許執行的那一輪,rawCount=1、keptCount=1,最後報告裡也確實有 SC2086。離線那一輪沒有安裝 ShellCheck,根本沒進行檢查;不能看到 0 findings 就認為程式碼沒有問題。

另一個容易漏掉的細節是嚴重程度。SC2086 在這套對應中屬於 nitpick,但 OpenQodex 預設的 review.severity_threshold 是 minor。只看預設可見報告,很可能以為什麼都沒抓到。先前我們就遇過這層篩選,因此測試改為明確指定 --block-on-severity nitpick,讓刻意製造的問題留在報告中。

現在我看掃描報告,會先確認掃描器是否真的跑了,再看原始發現經過嚴重程度與修改行篩選後剩下哪些。單看指令退出 0,在掃描器根本沒安裝的情況下,結論可能完全相反。

真正的 pre-push Hook 不會在每次推送時自動啟動 AI

第二輪直接用 Git 推送到本機的 bare remote,而不是碰正式專案或託管平台上的倉庫。

裝好 OpenQodex 的 pre-push Hook 後,我先提交一份沒有審閱紀錄的修改。在預設設定下,Hook 顯示未經審閱的提醒,但 Git push 仍然成功,本機 bare remote 的分支也真的往前更新。

接著加入這個設定:

version: 1
review:
  block_on_severity: major

再次提交新的未審閱修改,這次推送被拒絕;測試同時確認 bare remote 的 main 仍指向推送前的提交。這兩個結果都來自真正安裝的 Git pre-push Hook,不是自己寫一支永遠回傳 1 的假腳本。

合成推送情境實際結果
預設設定,沒有審閱收據提示警告,但允許推送
設定 block_on_severity: major,仍未審閱拒絕推送;遠端 HEAD 沒變

注意,這只驗證了缺少審閱收據時的兩種行為,沒有啟動 Codex,也沒有產生一份獨立模型完整審閱後的有效 Receipt。

從固定版本的 Hook 原始碼 可以看到:pre-push 會檢視即將推送的 Git 範圍,尋找使用者 OpenQodex HOME 裡對應修改的審閱收據,再決定是否阻擋。它不會因為你輸入 git push,就自動重跑掃描器、再啟動一個獨立 AI 模型。

所以「scan 執行成功」、「review 完整完成」、「hook 允許推送」是三件需要各自驗證的事情,不能全部寫成「已通過 AI 檢查」。

Reviewer 失敗時,為何 Hook 還可能放行?

真正麻煩的情況出現在第三輪。本機 Mac 嘗試啟動獨立 Codex Reviewer 時,子程序已經開始發送請求,卻收到 HTTP 401。OpenQodex 將審閱結果標為 incomplete,沒有取得有效的完整審閱收據。因此,這次無法評價獨立模型偵測 Bug 的準確度,也無法證明有效 Receipt 與過期 Receipt 的完整生命週期。

Mac 測試摘要還記錄了一個值得留意的現象:先前未審閱的推送遭到拒絕,後來在 incomplete 審閱之後,另一個 push 回傳 0,並建立遠端分支。但這兩次推送的目標 ref 不完全相同,而且原始 Mac 報告尚未納入可獨立驗證的證據包。我不會把它寫成已完整重現的「審閱失敗導致推送成功」因果實驗。

比較確定的是 OpenQodex 0.11.0 的原始碼規則。

在固定版本的 push-gate.ts 裡,checkPush() 遇到與目前修改相符、kind 為 incomplete 的審閱收據時,會回傳 abstain,也就是不主動阻止推送。即使設定 review.block_on_severity: major,這條分支也不會因此改成拒絕。

此外,hook.ts 產生的 Git Hook 只有在子命令退出碼為 1 時才會拒絕推送;工具本身出錯、子程序無法啟動等其他退出碼,外層腳本則回傳 0。這可以避免審閱工具故障時讓開發者完全無法使用 Git,但代價就是:設定嚴重程度門檻,不代表強制要求一次完整成功的 AI 審閱。

這是產品策略的差異,不是已證實的任意程式碼繞過漏洞。維護者可能刻意選擇「審閱服務壞掉時先放行」,但團隊也可能需要「審閱失敗時必須阻擋」。真正的風險,是把前者誤以為後者。

如果真的要求強制審閱,我會怎麼做

至少現階段,我會把 OpenQodex 當成開發者回饋工具,而不是唯一、無法繞過的合併門禁。掃描器能回報具體規則與檔案位置;Hook 能提醒這次修改沒有審閱。但要不要合併,應由另一道可觀察、可測試的 CI 檢查決定,明確驗證:

  • 必要掃描器是否真的執行,或其實是 not_installed、略過、執行失敗。
  • 獨立 AI Reviewer 是否完整結束,而不是 unavailable 或 incomplete。
  • 審閱結果是否對應目前的 commit / diff,使用的門檻是否符合現行政策。
  • CI 本身或審閱服務失敗時,受保護分支是否仍要求相關 required check 通過。

這是部署建議,並不是本次已完成的 GitHub 分支保護整合測試。 如果團隊規定每次合併都必須有完成的獨立審閱,就需要另外驗證 CI 是否真的具有 fail-closed 語意,不能只依賴本機的 pre-push 設定。

對個人開發倉庫而言,我甚至不一定會要求每次 push 都呼叫模型。需要時再跑 review、用靜態掃描提供快速回饋、把關鍵分支交給有實測的 CI 門禁,也許更符合成本與工作節奏。但不能因為「有安裝 Hook」,就說「獨立 AI 已經檢查過」。

這次真正確認的範圍很有限:固定版本 OpenQodex 0.11.0 在合成 Shell 修改中找到了 SC2086;隔離 Git 倉庫中的 Hook 重現預設警告放行與設定後拒絕未審閱推送;固定版本原始碼也明確包含 incomplete 或某些 Hook 錯誤時的放行路徑。獨立模型審閱並沒有完整跑通,沒有可信的模型品質、誤報率、延遲或有效 Receipt 往返驗收結論。

也因為這一段沒通過驗收,我暫時不會把 OpenQodex 的本機 Hook 當成正式環境唯一一道強制 AI 審閱門禁。它作為掃描器和審閱入口仍有價值,但在決定程式碼能否推送之前,最好先確認審閱沒完成時,究竟是誰負責擋下來。