用 Hooks 幫 AI 代理裝上煞車:兩個真實護欄的設計筆記
AI 代理最可怕的不是它做不到,而是它「太敢做」——這篇筆記記錄我怎麼用兩個 hook,把煞車裝回自己手上。
前言
我在工作中大量使用 Claude Code 處理專案管理文件、工單系統操作。用得越深,越發現一件事:寫在 prompt 裡的規則,模型不一定每次都遵守。真正踩過的雷有兩個:
- 有一次請代理「確認外部協作平台的 webhook 是否可用」,它直接發了一則訊息到正式頻道——未經授權,而且不可撤回。
- 代理反覆宣告「驗證完成、已對齊」,但實際上根本沒打開流程圖的源碼、沒跑檢查腳本。事後盤點長期使用紀錄,「誤解需求」與「方法錯誤」加起來出現了五十多次。
規則寫進 CLAUDE.md 之後有改善,但只要模型換個措辭、或上下文太長把規則擠掉,還是會破功。最後的解法是:把紅線從 prompt 搬進 hooks——用程式攔截,而不是用文字拜託。
護欄一:PreToolUse 攔截對外寫入
第一個護欄的核心觀念是:讀取自由,寫入攔截。read / search / list 這類動作不影響任何人,放行;但只要是「別人會看到、不可撤回」的動作——建工單、留言、更新頁面、發訊息——一律先停下來問。
作法是掛一個 PreToolUse hook,比對兩種情況:
- 工具名稱屬於工單系統的 MCP 寫入工具(建單、留言、更新頁面、上傳附件……)
- Bash 指令是對外部協作平台網域發出的
curl -X POST/PUT/DELETE/PATCH
命中就回傳 decision: "ask",附上一份檢查清單,強制走人工確認。簡化後的邏輯長這樣:
1 | input=$(cat) |
搭配一條寫進規則檔的原則,我覺得是整個設計裡最重要的一句話:
Prompt 裡的「執行」是意圖宣告,不是發射授權。
使用者在需求描述裡寫「執行」,代表「我想做這件事」,不代表「現在就送出去」。代理必須先列出「我要做什麼、目標物件、實際 payload 預覽」,停下來等使用者明確說「送」,才能動手。不論動作多小——哪怕只是五個字的留言——都一樣。
護欄二:Stop hook 擋下「我做完了」
第二個護欄處理的是更隱微的問題:代理宣告完成,但沒有真的檢查。
這個 hook 掛在 Stop 事件上,在代理準備結束回合時,解析 transcript 檢查這一回合到底做了什麼。觸發條件有兩條路(OR):
- 武裝模式:使用者的 prompt 含驗證類字眼(驗證、檢查、對齊、你確定、改好了嗎、verify、audit……)→ 這回合結束前直接查核,不論代理最後怎麼措辭。
- 窄關鍵字:代理的回覆含「驗證完成」「已對齊」「all consistent」這類宣告詞。
任一命中,就檢查「三件套」是否真的做了:
- 跑過檢查腳本(
verify.sh) - 本回合至少
Read過一份檔案 - 若話題涉及 PRD / spec / 流程圖,必須實際讀過
.svg或.mmd源碼,不能只看檔名
缺任何一項,回傳 {"decision": "block"} 並列出缺了什麼,把代理打回去補做。
實作上有兩個細節值得記:
- 後設語境豁免:如果使用者是在討論這個 hook 本身(出現 hook、觸發詞、誤觸、假陽性這類字眼),就不武裝——不然連調整護欄的對話都會被護欄擋住。
- 防無限迴圈:Stop hook 的輸入帶有
stop_hook_active旗標,已經被擋過一次的回合直接放行,避免 block → 重跑 → 又 block 的死循環。
1 | if [ "$stop_active" = "true" ]; then |
心得:護欄要寫在模型摸不到的地方
兩個護欄跑了一陣子之後,我的結論是:
- Prompt 規則是「請求」,hook 是「執法」。 模型會換句話說、會在長上下文裡遺忘,但 hook 是確定性的程式,不受措辭影響。武裝模式的設計正是針對這點——只要使用者要求驗證,不管代理最後怎麼包裝結論,都得過三件套這一關。
- 攔截點選在「動作」而不是「意圖」。 PreToolUse 看的是實際要呼叫的工具與指令,這是行為發生前的最後一道關卡,比在 prompt 層猜測意圖可靠得多。
- 護欄自己也要防呆。 meta 豁免與
stop_hook_active這兩個小設計,都是上線後被自己的護欄卡到才補的。
AI 代理的能力越強,越需要這種「模型改不了、繞不過」的外部結構。與其期待模型永遠自律,不如把紅線寫成程式。下一篇想聊聊怎麼用 YAML 當單一事實來源,讓決策文件自動串聯稽核。你的代理有裝什麼煞車嗎?歡迎交流。
參考資料: