2026-08-23 雲端驗收連續三次被這道閘誤攔,三次都不是宣告意圖:
① 否認自己有下一步 ②引用閘自己的訊息 ③貼閘自己的正則原始碼舉報 bug。
而訊息教人走的「選項③:說明它在等什麼」,程式碼裡根本沒有那條分支
——唯一走得通的路是不寫那三個字,正是同一則訊息明文禁止的動作。
四個真兇,沒有一個是「例外沒列夠」:
(a) DECL 會匹配裸的「下一步」三個字(每一節都可選 ⇒ 退化成關鍵字)
⇒ 收緊:每一條 alternative 都必須接到動作動詞才算命中
(b) 只剝 > 引言與長「」,不認 code fence 與行內 code ⇒ 引用被當成主張
⇒ 引用性標記整段換成哨兵(不是刪掉):內層宣告消失、外層句構留著
——刪掉正是 08-17 漏掉「回『規劃』我就派人」的原因,兩個方向一起修
(c) 取 blocks_text[-1],但那則文字後面可能還有 tool_use ⇒ 宣告其實兌現了
⇒ 只看「最後一個動作之後」的文字;收尾在動作上就不觸發
(d) 訊息承諾的出路只有兩條真的存在
⇒ 出路③ 給一個機械形式 ⏸ 等:<在等什麼>(白名單標記,要刻意寫,留痕)
方向刻意與「再加幾個關鍵字例外」相反——例外清單會越加越長、越長越誤攔。
守 leo 的封路哲學:紅線寫得越細,命中關鍵字的機率越高 ⇒ 那些閘在懲罰謹慎。
順手:擋下與放行都留痕(InkStoneCo#48:只記擋下的話分母未知);
log 目錄不在時安靜跳過,不再噴 redirect 錯誤到 stderr。
測試 hooks/tests/factory-idle-guard.test.sh 27 向,誤攔與漏攔兩個方向都測:
舊版 18/27(9 敗)→ 新版 27/27。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
19 KiB
43 支閘,白話盤點表
回應
inkstone/InkStoneCo#40:「如果加入了,我應該可以白話文看到 hooks 的內容?」 這份表就是那個「白話文」——不用點開任何.sh檔,一行看懂一支閘在管什麼。最高原則(票上原文):每一條規則你都要能在 30 秒內看懂它在管什麼。
一句話結論
hooks/ 底下有 43 個 .sh 檔,hooks.json 實際掛上 53 條註冊(同一支閘常被多種情境同時掛上);
其中 3 支檔案存在但沒被掛上(2 支是待人填的空範本、1 支是刻意留著沒開的止血帶,見下面「未生效」表)。
下面按「你會在什麼時候撞到它」分組,一支一行。
怎麼讀這張表
| 符號 | 意思 |
|---|---|
| 🛑 擋 | 條件不滿足就真的擋下這個動作(exit 2),你或 AI 會看到一段紅字說明 |
| 📝 記錄 | 不擋任何東西,只是在背景寫一筆紀錄或送一段提示文字給 AI |
| 💀 未生效 | 檔案存在,但沒有掛進 hooks.json——目前是死的,不會被執行 |
「對你意味著什麼」欄一律用「如果你看到 X,代表 Y」的角度寫,不寫程式邏輯。
A. 你(或 AI)在終端機打指令的當下(PreToolUse / Bash)
| 閘名 | 對你意味著什麼 | 動作 |
|---|---|---|
github-contact-guard.sh |
AI 想碰 GitHub(git push、gh api 等)就先擋下——兩個帳號曾經因為機器人式高頻寫入被永久 flag 過。只有讀取(clone/fetch)自由,寫入一律要你在終端機跑 github-arm.sh 解鎖。 |
🛑 擋 |
kbdb-api-wall-guard.sh |
AI 想繞過 KBDB 的 API、直接對資料庫下 SQL 或建新表就擋下——KBDB 的鐵律是「只能用 API、永遠只有三張表」。 | 🛑 擋 |
stage-before-prod-guard.sh |
AI 想動「出貨鏈」相關指令(含 arcrun-rag-bundles/github-arm/publish-github 字樣)卻沒先在 stage 環境驗過就擋下。 |
🛑 擋 |
main-and-prod-push-guard.sh |
兩層確認:① subagent 想推 Gitea main 分支 → 一定要交回總管,總管確認過才放行;② 任何 wrangler deploy 打正式環境 → 一定要你親手解鎖。 |
🛑 擋 |
prod-write-guard.sh |
AI 想做任何「會真的改到線上正式帳號(leo21c)」的操作(不只是 push,也包含直接呼叫會寫入的工具)就擋下,交回總管處理。 | 🛑 擋 |
not-my-branch-guard.sh |
AI 想 commit 到「別的 subagent 正在施工中」的分支(登記在 branch-holds.md)就擋下,避免兩個 AI 互踩同一條分支。 |
🛑 擋 |
leo21c-write-guard.sh |
AI 想寫東西到你私人帳號 leo21c 就擋下——這個帳號只准你自己用,AI 的實驗一律走別的測試帳號。 |
🛑 擋 |
release-tag-guard.sh |
AI 想打版本 tag,但 plugin.json 裡寫的版本號跟 tag 對不上就擋下——避免「這裡說 0.2.0、那裡說 0.1.0」各說各話。 |
🛑 擋 |
ticket-api-bypass-guard.sh |
AI 想繞過 scripts/ticket 工具、直接打 API 開新 Gitea 票(會漏掉「開票前先搜過」這道檢查)就擋下。 |
🛑 擋 |
B. AI 想寫檔案/改程式碼的當下(PreToolUse / Write·Edit·MultiEdit)
| 閘名 | 對你意味著什麼 | 動作 |
|---|---|---|
guard-cross-project.sh |
總管(頂層)想直接改某個子 repo 的程式碼(非 .md)就擋下——頂層只做安排交辦,實作要進那個子 repo 自己做。 |
🛑 擋 |
wiki-secret-scan.sh |
要寫進 system-dev/wiki/ 的內容裡出現密碼/金鑰/身分證/信用卡等特徵就擋下,防止機敏資料意外留在會被反覆讀取的記憶空間裡。 |
🛑 擋 |
component-guard.sh |
AI 想自己新建一個零件(component)或亂接 service binding 就擋下——逼它先想「現成零件夠不夠用」,真要建要你解鎖。 | 🛑 擋 |
sdd-guard.sh |
AI 想直接動程式碼檔案,但找不到「唯一一份 active 規格文件(SDD)」對應這件事,或同時有一份以上 active 規格就擋下。 | 🛑 擋 |
credential-only-guard.sh |
AI 想把金鑰真身或自製佔位符(例如 __XXX_TOKEN__)寫進設定檔就擋下——金鑰只准放在統一的 credential 中心。 |
🛑 擋 |
arcrun-intent-guard.sh |
AI 寫的 Arcrun workflow 語法不對就擋下,而且直接把正確寫法回貼給它(不是只罵它錯,是教它怎麼改)。 | 🛑 擋(教學型) |
subagent-first-guard.sh |
這個對話從頭到尾都沒有派過任何 subagent,AI 卻要自己動手改程式碼,就先擋一次,逼它想一想「這件事能不能交給別人做」。 | 🛑 擋 |
mistake-needs-ticket-guard.sh |
AI 想往 mistakes.md(教訓紀錄)新增一條「機制可以防止」的教訓,卻沒附對應票號就擋下——沒有票號的教訓沒有人會回頭處理。 |
🛑 擋 |
pending-changes-retired.sh |
AI 想寫東西進已經廢除的 pending-changes.md 檔案就擋下——這個檔案已停用,規格變更一律改開 Gitea 票。 |
🛑 擋 |
kbdb-api-wall-guard.sh在這裡也重複掛了一次(見 A 組)——它同時守著「下指令」跟「寫檔案」兩種情境,詳見下方「重複掛載」一節。
C. AI 要去翻程式碼/查資料之前(PreToolUse / Grep·Glob·Read·Bash)
| 閘名 | 對你意味著什麼 | 動作 |
|---|---|---|
wiki-first-search.sh |
AI 這回合還沒查過 wiki就要去翻程式碼或查外部資料,就先擋下、逼它用你的關鍵字先搜一次 wiki(查過一次、不論有沒有找到,這回合後面就放行)。 | 🛑 擋 |
D. AI 要派工給別的 AI(subagent)之前(PreToolUse / Agent·Task)
| 閘名 | 對你意味著什麼 | 動作 |
|---|---|---|
subagent-wiki-guard.sh |
AI 要派一個查證/實作類任務出去,就自動在派工單裡塞一句「先查 wiki」的提示——不擋,只是順手夾帶叮嚀。 | 📝 記錄 |
kbdb-api-wall-guard.sh |
同 A/B 組,只是這裡管的是「派工單裡有沒有寫出違反 KBDB 規約的指示」。 | 🛑 擋 |
micromanage-guard.sh |
派工單寫得太細(指名檔案函式、編號步驟、要求每做一項回報一次…)就擋下——subagent 該被當成有能力的同事,不是照抄劇本的工具。 | 🛑 擋 |
irreversible-dispatch-guard.sh |
派工單裡出現「刪分支」「drop table」「rm -rf」這類不可逆動作,卻沒寫「先停下來等回覆才執行」就擋下。 | 🛑 擋 |
no-ticket-no-dispatch.sh |
派工單裡沒有寫工單號(【工單】owner/repo#N),或那張票已經關閉/根本不存在,就擋下——沒有票號的工作沒有人追得到進度。 |
🛑 擋 |
E. 每個對話一開始(SessionStart)
| 閘名 | 對你意味著什麼 | 動作 |
|---|---|---|
session-start-recall.sh |
對話一開始就自動把「全局現況」(Gitea 各 repo 的票、KBDB 的藏書地圖)推到 AI 眼前,不必等它自己想到要查。 | 📝 記錄(context 注入) |
skill-deploy-drift-guard.sh |
如果「全機真正在用的 skill」跟「repo 裡版控的正本」內容對不上,就在開場講出來——避免用著一份沒人知道已經跟正本分家的舊拷貝。 | 📝 記錄 |
F. AI 想結束這一輪、要收工的時候(Stop)
| 閘名 | 對你意味著什麼 | 動作 |
|---|---|---|
empty-handed-stop-guard.sh |
這一輪 AI一個動作都沒做卻想停下來(等你回覆),就擋下並告訴它「你的命令就是完整授權,不用再等第二次確認」。 | 🛑 擋(至多攔一次) |
worklist-guard.sh |
AI 自己列過的待辦清單裡還有沒做完的步驟,卻想收工寫報告,就擋下,逼它做完剩下的步驟。 | 🛑 擋 |
factory-idle-guard.sh |
AI 該去派工卻沒派(工頭停工),就擋下要它交出「已經派工的憑證」,不是隨口說一句「我會催」就算數。過閘有四條路:現在就派工/把票號寫進那句話/寫一行 ⏸ 等:<在等什麼>/這一輪收尾在動作上。 2026-08-23(inkstone/ISEP#30)修好「引用被當成主張」——貼原始碼、引用它自己的訊息、否認自己有下一步,都不再被咬。 |
🛑 擋 |
browser-verify-guard.sh |
這一輪 AI 宣稱「前端驗過了」,卻沒有真的用瀏覽器工具載入過,就擋下——curl 抓到 HTML 不算驗過。 |
🛑 擋 |
self-drive-police.sh |
AI 想停下來問你「早就決定過的事」(用固定句型判斷,例如「要不要 X」「下一步做什麼」「這交給你」)就擋下,反問它查過 wiki/查過派工表了沒。 | 🛑 擋 |
self-drive-judge.sh |
跟上面同一件事,但改用小模型(haiku)判斷「換句話說」的請示句——防止 AI 只是把「要不要」改寫成「不確定是否符合期待」就閃過上一支閘。 | 🛑 擋 |
delivery-police.sh |
AI 宣稱「這件事做完了」,卻看不到任何實測證據(畫面截圖、指令輸出、HTTP 狀態碼…)就擋下。 | 🛑 擋 |
wiki-first-police.sh |
AI 做完事卻沒有把結論寫回 wiki 就想收工,就擋下——下次(或別的 AI)查 wiki 會查不到這次做過什麼。 | 🛑 擋 |
unpushed-police.sh |
AI 改好的東西還留在本機、沒有真的推送出去給別人用,卻想收工,就擋下——「改對了但沒送到」跟沒改是一樣的。 | 🛑 擋 |
claim-verify-police.sh |
Subagent 交回來的「我做完了」宣稱還沒被驗證過(對應的待驗檔案還在),你這邊卻想收工,就擋下。 | 🛑 擋 |
G. Subagent 把工作交回來的時候(SubagentStop)
| 閘名 | 對你意味著什麼 | 動作 |
|---|---|---|
subagent-claim-worksheet.sh |
Subagent 一交回工作,就自動把它宣稱做了什麼寫成一張「待驗清單」檔案——之後總管收工前,claim-verify-police.sh 會檢查這張清單有沒有被處理掉。 |
📝 記錄 |
這個時機還掛了
worklist-guard.sh/self-drive-police.sh/self-drive-judge.sh/delivery-police.sh/wiki-first-police.sh/unpushed-police.sh,行為跟上面 F 組完全一樣,只是對象換成「subagent 交回來的這一輪」。詳見「重複掛載」一節。
H. 動作做完之後,純粹記一筆(PostToolUse,全部不擋)
| 閘名 | 對你意味著什麼 | 動作 |
|---|---|---|
kbdb-asked-stamp.sh |
AI 真的查過 KBDB 之後,留一個時間戳——給前面 history-first-guard.sh 判斷「這輪有沒有先查過」用。 |
📝 記錄 |
subagent-first-stamp.sh |
AI 真的派過工之後,留一個時間戳——給 subagent-first-guard.sh 判斷用。 |
📝 記錄 |
issue-status-autoflip.sh |
AI 一派工出去,就自動把對應的 Gitea 票改成「進行中(s/doing)」,不必等人手動改標籤。 | 📝 記錄(自動改票) |
I. 你會撞到但跟「派工/收工」無關的一支(Edit·MultiEdit)
| 閘名 | 對你意味著什麼 | 動作 |
|---|---|---|
history-first-guard.sh |
AI 要改一個舊檔案之前,先把這個檔案過去被改過幾次、被誰在什麼情況下改過的紀錄攤在它眼前,逼它回答「這是不是已經修過的老問題」再動手。 | 🛑 擋 |
目前沒生效的 3 支(存在但沒掛進 hooks.json)
| 檔名 | 為什麼沒掛 |
|---|---|
pre-write-guard.template.sh |
官方留的空殼範本,預設不攔任何東西。要用要自己手填禁令清單、自己去掛。檔頭寫明「別誤以為裝了它就有保護」。 |
pre-write-guard.sh |
同一個範本的另一份(看起來是填過一半的版本),同樣沒掛進 hooks.json。 |
shadow-table-guard.sh |
檔頭自己寫明「本檔目前是死的」——leo 2026-08-15 說過「提案封鎖方式,不是要你就去做」,所以先寫好、測過,但要不要真的掛上由你裁。 |
落差偵測(票上要求的「這件事本身值得被看到」)
方法:比對 hooks/*.sh 檔案清單 vs hooks.json 裡出現的檔名。
$ ls hooks/*.sh | xargs -n1 basename | sort > /tmp/fs_hooks.txt
$ grep -oE '[a-zA-Z0-9_-]+\.sh' hooks/hooks.json | sort -u > /tmp/registered_hooks.txt
# 有檔案、但 hooks.json 沒註冊到 → 上面「沒生效的 3 支」
$ comm -23 /tmp/fs_hooks.txt /tmp/registered_hooks.txt
pre-write-guard.sh
pre-write-guard.template.sh
shadow-table-guard.sh
# hooks.json 註冊了、但檔案不存在 → 空(目前沒有這種「指向空氣」的閘)
$ comm -13 /tmp/fs_hooks.txt /tmp/registered_hooks.txt
(無輸出)
這次順便抓到的另一個落差:.claude-plugin/plugin.json 的說明文字寫「42 支機械閘(52 條註冊)」,
README.md 也寫「42 支 + hooks.json」「共 52 條註冊」——但實測是 43 支檔案、53 條註冊。
差 1 支、差 1 條,猜測是今天(2026-08-20)新增的 leo21c-write-guard.sh/release-tag-guard.sh/
ticket-api-bypass-guard.sh 這批(檔頭日期都是今天)加了之後,兩份文件的數字沒有跟著更新。
這兩個檔案本次刻意沒動(在你劃的紅線內:不准碰 .claude-plugin/),只在這裡把落差標出來給你看。
重複掛載(同一支閘在不只一個時機生效)
不是衝突,是同一支閘刻意守好幾個情境;列出來是因為你可能會納悶「怎麼同一句紅字出現在不同地方」:
| 閘名 | 掛了幾次 | 為什麼 |
|---|---|---|
kbdb-api-wall-guard.sh |
3 次 | 下指令(Bash)、寫檔案(Write/Edit)、派工(Task)三種情境都可能違反 KBDB 規約,各掛一次 |
prod-write-guard.sh |
2 次 | 一次守「下指令」,一次專門守「會直接寫進線上的 MCP 工具」(不是走終端機指令的那種) |
arcrun-intent-guard.sh |
2 次 | 一次守「所有寫檔案」,一次專門加強守「呼叫 Arcrun 部署/驗證工具」這個更精準的情境 |
micromanage-guard.sh / irreversible-dispatch-guard.sh / no-ticket-no-dispatch.sh |
各 2 次 | 各自同時掛在 Task 與 Agent 兩個矩比對名稱上——這兩個名稱應該是同一種派工動作的新舊叫法,兩個都掛保證不漏接 |
worklist-guard.sh / self-drive-police.sh / self-drive-judge.sh / delivery-police.sh / wiki-first-police.sh / unpushed-police.sh |
各 2 次 | 一次守「總管自己想收工」(Stop),一次守「subagent 交回工作」(SubagentStop)——同一套判準用在兩種角色身上 |
❓ 這裡有一件我看不出來是刻意還是遺留、需要人判斷:micromanage-guard.sh/irreversible-dispatch-guard.sh/no-ticket-no-dispatch.sh
三支都同時掛在 Task 跟 Agent 這兩個矩比對名稱上。如果這兩個名稱在目前版本的 Claude Code 裡指的是「同一種派工工具呼叫」,
那這是保險(兩個名字都接住,不怕哪天官方改名),沒問題;但如果其實只有一個名稱會真的觸發,另一個是舊名稱留下來沒清掉,
那就是「規則說是兩層防護,實際只有一層在動」。這個要靠實際觸發紀錄核對,我沒有把握單靠讀檔案判斷,標成 ❓ 而不是硬下結論。
抽驗 5 支:一句話 vs 實際邏輯逐條對照
隨機抽了跨越不同層級/時機的 5 支,逐行核對過源碼(不是只讀檔頭):
github-contact-guard.sh——表格寫「AI 想碰 GitHub 就先擋下,只有讀取自由」。 源碼核對:只擋gh api/repo/issue/pr/...子指令,以及git push/git remote add指向 github.com(含用 remote 名稱反解出網址的情況);git clone/fetch/pull/ls-remote、curl、go get一律放行。帶憑證的「實名讀」也放行但會留一筆紀錄。與表格描述一致。main-and-prod-push-guard.sh——表格寫「推 main 要總管戳記、prod 部署要你解鎖」。 源碼核對:git push目標含main/master才擋,且要/tmp/.main-push-ok戳記綁對 repo 路徑、15 分鐘內、用過即丟才放行;wrangler deploy/publish/versions deploy且指令裡看不出打的是 stage/youlin/geek6688 就擋,要.github-armed或 Gitea 票上核准碼才放行。與表格描述一致,且比表格寫得更細(例如 stage 與 geek6688 兩個白名單)。empty-handed-stop-guard.sh——表格寫「這輪零動作卻想停就擋,至多攔一次」。 源碼核對:讀 transcript 數這回合tool_use出現次數,0 次 → exit 2;stop_hook_active已為真(代表已經擋過一次)→ 直接放行,不會卡死。與表格描述一致。no-ticket-no-dispatch.sh——表格寫「派工單沒工單號,或票已關閉/不存在就擋」。 源碼核對:從tool_input.prompt抓【工單】owner/repo#N,抓不到 → exit 2;抓到但打 Gitea API 查到state=closed或查不到(missing)→ exit 2; 查不到網路(token 拿不到、API 連不上)→ 放行(fail-open,避免網路抖動卡死工作)。與表格描述一致,且多了一個表格沒特別寫的細節:網路問題不擋。wiki-secret-scan.sh——表格寫「寫進 wiki 的內容有密碼/金鑰/身分證/信用卡特徵就擋」。 源碼核對:只在file_path命中system-dev/wiki/*時啟動;用 6 類 regex(密碼賦值、PEM 私鑰、雲端金鑰前綴、JWT、連線字串內嵌帳密、身分證、信用卡)逐條檢查要寫入的內容; 行尾標記wiki-secret-ok可豁免。與表格描述一致。
五支全部核對通過,沒有發現表格描述跟實際邏輯對不上的情況。
我看不懂、需要人看的地方
micromanage-guard.sh/irreversible-dispatch-guard.sh/no-ticket-no-dispatch.sh的雙重矩比對(Task+Agent)是刻意保險還是舊名稱沒清掉——見上面「重複掛載」段落,我沒有把握單靠讀檔案判斷,需要看實際觸發紀錄或問總管。stage-before-prod-guard.sh跟main-and-prod-push-guard.sh的分工邊界:main-and-prod-push-guard.sh的檔頭明講自己是在「補stage-before-prod-guard.sh的破口」(那支只認 3 個關鍵字,抓不到wrangler deploy),但兩支都還掛著、都還在管「prod 出貨」這件事。這是「新的補洞、舊的continua」還是「舊的該退休了」,這份盤點表不下判斷,留給你在下一步的分類會議裡定奪。
除了以上兩點,其餘 41 支的行為都能從檔頭與源碼直接讀出,沒有「猜」的部分。
這份表怎麼跟實況對帳(半年後怎麼發現漂移)
- 有沒有新閘沒被收進這張表:跑本文「落差偵測」段落的兩行
comm指令,比對hooks/*.sh的檔名清單跟這張表列出的閘名清單(不是跟hooks.json,那個只驗證有沒有註冊,驗不了有沒有寫進這張人話表)。 - 有沒有閘的行為跟這裡寫的不一樣了:抽幾支重新讀一次源碼,跟這裡的「一句話」對一遍——就是本文「抽驗 5 支」做的事,可以照同樣方法定期重做。
.claude-plugin/plugin.json與README.md的數字:這兩處各自寫了一次「幾支、幾條註冊」,前面已經抓到一次對不上(42/52 vs 實際 43/53)。這兩個數字沒有機制保證跟著hooks/目錄自動更新,是本表發現的第一個具體漂移案例。