GateGuard — 強制事實查證的預操作閘門 是做什麼的?
一個 PreToolUse 鉤子,強制 Claude 在編輯前進行調查。不是自我評估(「你確定嗎?」),而是要求具體事實。調查這個行為本身就創造了自我評估從未達到的警覺性。
何時啟用
- 在任何程式碼庫中工作,檔案編輯會影響多個模組時
- 專案具有特定結構或日期格式的資料檔案時
- 團隊要求 AI 產生的程式碼必須符合既有模式時
- 任何 Claude 傾向於猜測而非調查的工作流程中
核心概念
LLM 的自我評估無效。問「你違反了任何政策嗎?」答案永遠是「沒有」。這已透過實驗證實。
但問「列出所有導入此模組的檔案」會強制 LLM 執行 Grep 和 Read。調查本身創造了改變輸出的情境。
三階段閘門:
1. 拒絕 — 封鎖第一次 Edit/Write/Bash 嘗試
2. 強制 — 明確告知模型要收集哪些事實
3. 允許 — 在事實呈現後允許重試
沒有競爭對手做到全部三點。大多數僅止於拒絕。
證據
兩個獨立的 A/B 測試,相同的代理人,相同的任務:
| 任務 | 有閘門 | 無閘門 | 差距 |
|---|---|---|---|
| 分析模組 | 8.0/10 | 6.5/10 | +1.5 |
| Webhook 驗證器 | 10.0/10 | 7.0/10 | +3.0 |
| 平均 | 9.0 | 6.75 | +2.25 |
兩個代理人生成的程式碼都能執行並通過測試。差異在於設計深度。
閘門類型
編輯 / MultiEdit 閘門(每個檔案第一次編輯)
MultiEdit 以相同方式處理 — 批次中的每個檔案獨立被閘門攔截。
在編輯 {file_path} 之前,請呈現以下事實:
1. 列出所有導入/需要此檔案的檔案(搜尋整個目錄樹 — 使用 Glob/Grep,或透過 Bash 的 find/grep)
2. 列出此變更影響的公開函式/類別
3. 如果此檔案讀取/寫入資料檔案,請顯示欄位名稱、結構
和日期格式(使用修訂過或合成的值,而非原始生產資料)
4. 逐字引用使用者的當前指示
寫入閘門(第一次建立新檔案時)
在建立 {file_path} 之前,請呈現以下事實:
1. 命名將呼叫此新檔案的檔案和行號
2. 確認沒有既有檔案提供相同功能(搜尋整個目錄樹 — 使用 Glob/Grep,或透過 Bash 的 find/grep)
3. 如果此檔案讀取/寫入資料檔案,請顯示欄位名稱、結構
和日期格式(使用修訂過或合成的值,而非原始生產資料)
4. 逐字引用使用者的當前指示
破壞性 Bash 閘門(每次破壞性命令)
觸發條件:rm -rf、git reset --hard、git push --force、drop table 等。
1. 列出此命令將會修改或刪除的所有檔案/資料
2. 寫出一行復原程序
3. 逐字引用使用者的當前指示
常規 Bash 閘門(每個會話一次)
1. 一句話總結當前的使用者請求
2. 此特定命令驗證或產生什麼
平行批次與部分應用
首次接觸閘門會獨立評估每個工具呼叫。當一個平行批次中包含多個針對尚未接觸過之檔案的編輯時,第一個呼叫會被拒絕,並將該檔案標記為已檢查,因此該批次中的兄弟編輯會被套用。不會有任何回滾:檔案最終可能同時包含兄弟編輯(但不包含被拒絕的那個)。
拒絕訊息會命名該檔案並警告批次兄弟可能已被套用。請嚴格遵循以下指示:
- 將針對尚未接觸檔案的相依編輯以序列方式(而非平行批次)發送。定義與其首次使用、或匯入與其呼叫點,不得在同一批次中。
- 在首次接觸被拒絕後,呈現事實、重試被拒絕的編輯,然後在基於批次中其他內容進行建構之前,重新讀取該檔案。
無法實現批次級鎖定:鉤子一次只能看到一個工具呼叫,因此閘門無法知道哪些呼叫是同時到達的。
快速開始
選項 A:使用 ECC 鉤子(零安裝)
位於 scripts/hooks/gateguard-fact-force.js 的鉤子已包含在此外掛中。透過 hooks.json 啟用。
如果 GateGuard 阻礙了設定或修復工作,請使用 ECC_GATEGUARD=off 開始會話。若要進行鉤子層級的控制,請繼續使用 ECC_DISABLED_HOOKS 搭配 GateGuard 鉤子 ID。
在長時間的會話中,只有前 GATEGUARD_FACT_FORCE_FULL_DENIALS 次(預設為 3 次)強制事實的拒絕會發出完整的四項事實區塊;之後的拒絕會濃縮為一行,攜帶拒絕序數,這樣相似的區塊就不會在情境視窗中累積並放大模型的重複迴圈(#2142)。在呈現事實後重試相同檔案或命令永遠不會重新觸發閘門。
逐步控制
ECC_GATEGUARD=off(或 GATEGUARD_DISABLED=1)完全關閉閘門。下表中的變數則不會完全關閉——每個變數只縮窄一個行為,而承載重要性的破壞性 Bash 檢查仍持續執行:
| 變數 | 預設值 | 效果 |
|---|---|---|
GATEGUARD_BASH_ROUTINE_DISABLED | 未設定(閘門開啟) | 僅停用常規 Bash 閘門。破壞性 Bash 閘門(rm -rf、git reset --hard、drop table、dd if= 等)不受影響。 |
GATEGUARD_EXEMPT_GLOBS | 未設定(無豁免) | 逗號分隔的 glob 模式;符合條件的編輯/寫入/MultiEdit 目標會跳過首次接觸的事實強制。適用於低匯入價值的目錄樹(測試、產出工件、暫存目錄),其中「誰導入此檔案/什麼結構」沒有訊息價值。 |
GATEGUARD_FACT_FORCE_FULL_DENIALS | 3 | 在後續拒絕濃縮為一行之前,有多少次拒絕會發出完整的四項事實區塊。設為 0 則從第一次拒絕就開始濃縮。 |
GATEGUARD_BASH_EXTRA_DESTRUCTIVE | 未設定 | 額外的破壞性命令模式(作為正則表達式來源),加入內建集合中。格式錯誤的正則表達式視為未設定(內建模式仍適用)並記錄一次到 stderr。 |
GATEGUARD_STATE_DIR | ~/.gateguard | 每個會話的閘門狀態儲存位置。如果無法持久化狀態,閘門會允許操作而非陷入迴圈,並在警告中提及此變數。 |
GATEGUARD_BASH_ROUTINE_DISABLED 接受 1、true、on、enabled、enable 或 yes(不區分大小寫和空白);任何其他值則維持閘門開啟。
完全關閉閘門
| 變數 | 效果 |
|---|---|
ECC_GATEGUARD=off | 為此會話停用 GateGuard。接受 0、false、off、disabled 或 disable。 |
GATEGUARD_DISABLED=1 | 相同效果。僅辨識 1 — 上述的拼寫方式不適用於此。 |
若要進行鉤子層級的控制,請繼續使用 ECC_DISABLED_HOOKS 搭配 GateGuard 鉤子 ID。
GATEGUARD_EXEMPT_GLOBS 的 Glob 語義
模式比對整個相對於專案的目標路徑。專案根目錄為 CLAUDE_PROJECT_DIR,如果沒有則回退到鉤子 payload 的 cwd,再回退到鉤子程序的工作目錄。相對 glob 永遠不會豁免該根目錄之外的目標。明確的絕對 glob 比對整個絕對目標路徑,且可能故意豁免專案之外的路徑。
模式和路徑都使用 / 分隔符號和不區分大小寫比對。* 在一個區段內比對,** 跨區段比對,? 比對一個非分隔符號的字元。**/ 包含零個目錄,所以 **/tests/** 也符合 tests/foo.js。格式錯誤的模式會被丟棄而不授予豁免。
自 2.2.1 版起,services/** 僅涵蓋專案根目錄的 services 目錄樹,而 *.md 僅涵蓋根目錄的 Markdown 檔案。使用 **/*.md 涵蓋專案內的所有 Markdown 檔案。現有的無錨點豁免可能需要調整:
{
"env": {
"GATEGUARD_BASH_ROUTINE_DISABLED": "1",
"GATEGUARD_EXEMPT_GLOBS": "**/tests/**,tests/**,**/*.test.*,**/docs/**,**/dist/**"
}
}
選項 B:完整套件搭配設定
pip install gateguard-ai
gateguard init
這會新增 .gateguard.yml 用於每個專案的設定(自訂訊息、忽略路徑、閘門開關)。
反模式
- 不要使用自我評估來代替。「你確定嗎?」永遠得到「是的。」這已透過實驗證實。
- **不要跳過資料結構檢查。**兩個 A/B 測試代理人都假設 ISO-8601 日期格式,但真實資料使用
%Y/%m/%d %H:%M。檢查資料結構(使用修訂過的值)可以防止此類錯誤。 - **不要對每個 Bash 命令都設閘門。**常規 Bash 閘門每個會話只觸發一次。破壞性 Bash 閘門每次都會觸發。這種平衡可以避免效能降低,同時捕捉真正的風險。
最佳實踐
- 讓閘門自然觸發。不要試圖預先回答閘門問題 — 調查本身才是提升品質的關鍵。
- 根據你的領域自訂閘門訊息。如果你的專案有特定慣例,請將它們加入閘門提示中。
- 使用
.gateguard.yml忽略.venv/、node_modules/、.git/等路徑。
相關技能
safety-guard— 執行時安全檢查(互補,不重疊)code-reviewer— 編輯後審查(GateGuard 是編輯前調查)