Files
ISEP/docs/hooks-inventory.md
T
Leo a594decb78 稼動率警察改成擋「宣告」,不擋「提到宣告」
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>
2026-08-23 17:42:22 +08:00

19 KiB
Raw Permalink Blame History

43 支閘,白話盤點表

回應 inkstone/InkStoneCo#40:「如果加入了,我應該可以白話文看到 hooks 的內容?」 這份表就是那個「白話文」——不用點開任何 .sh 檔,一行看懂一支閘在管什麼。

最高原則(票上原文):每一條規則你都要能在 30 秒內看懂它在管什麼。

一句話結論

hooks/ 底下有 43 個 .shhooks.json 實際掛上 53 條註冊(同一支閘常被多種情境同時掛上); 其中 3 支檔案存在但沒被掛上(2 支是待人填的空範本、1 支是刻意留著沒開的止血帶,見下面「未生效」表)。 下面按「你會在什麼時候撞到它」分組,一支一行。


怎麼讀這張表

符號 意思
🛑 條件不滿足就真的擋下這個動作(exit 2),你或 AI 會看到一段紅字說明
📝 記錄 不擋任何東西,只是在背景寫一筆紀錄或送一段提示文字給 AI
💀 未生效 檔案存在,但沒有掛進 hooks.json——目前是死的,不會被執行

「對你意味著什麼」欄一律用「如果你看到 X,代表 Y」的角度寫,不寫程式邏輯。


A. 你(或 AI)在終端機打指令的當下(PreToolUse / Bash

閘名 對你意味著什麼 動作
github-contact-guard.sh AI 想碰 GitHubgit pushgh 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-bundlesgithub-armpublish-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 要派工給別的 AIsubagent)之前(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-23inkstone/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.shself-drive-police.shself-drive-judge.shdelivery-police.shwiki-first-police.shunpushed-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.shrelease-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 次 各自同時掛在 TaskAgent 兩個矩比對名稱上——這兩個名稱應該是同一種派工動作的新舊叫法,兩個都掛保證不漏接
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.shirreversible-dispatch-guard.shno-ticket-no-dispatch.sh 三支都同時掛在 TaskAgent 這兩個矩比對名稱上。如果這兩個名稱在目前版本的 Claude Code 裡指的是「同一種派工工具呼叫」, 那這是保險(兩個名字都接住,不怕哪天官方改名),沒問題;但如果其實只有一個名稱會真的觸發,另一個是舊名稱留下來沒清掉, 那就是「規則說是兩層防護,實際只有一層在動」。這個要靠實際觸發紀錄核對,我沒有把握單靠讀檔案判斷,標成 而不是硬下結論。


抽驗 5 支:一句話 vs 實際邏輯逐條對照

隨機抽了跨越不同層級/時機的 5 支,逐行核對過源碼(不是只讀檔頭):

  1. github-contact-guard.sh——表格寫「AI 想碰 GitHub 就先擋下,只有讀取自由」。 源碼核對:只擋 gh api/repo/issue/pr/... 子指令,以及 git pushgit remote add 指向 github.com(含用 remote 名稱反解出網址的情況); git clone/fetch/pull/ls-remotecurlgo get 一律放行。帶憑證的「實名讀」也放行但會留一筆紀錄。與表格描述一致。
  2. 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 兩個白名單)。
  3. empty-handed-stop-guard.sh——表格寫「這輪零動作卻想停就擋,至多攔一次」。 源碼核對:讀 transcript 數這回合 tool_use 出現次數,0 次 → exit 2stop_hook_active 已為真(代表已經擋過一次)→ 直接放行,不會卡死。與表格描述一致。
  4. no-ticket-no-dispatch.sh——表格寫「派工單沒工單號,或票已關閉/不存在就擋」。 源碼核對:從 tool_input.prompt【工單】owner/repo#N,抓不到 → exit 2;抓到但打 Gitea API 查到 state=closed 或查不到(missing)→ exit 2; 查不到網路(token 拿不到、API 連不上)→ 放行(fail-open,避免網路抖動卡死工作)。與表格描述一致,且多了一個表格沒特別寫的細節:網路問題不擋。
  5. wiki-secret-scan.sh——表格寫「寫進 wiki 的內容有密碼/金鑰/身分證/信用卡特徵就擋」。 源碼核對:只在 file_path 命中 system-dev/wiki/* 時啟動;用 6 類 regex(密碼賦值、PEM 私鑰、雲端金鑰前綴、JWT、連線字串內嵌帳密、身分證、信用卡)逐條檢查要寫入的內容; 行尾標記 wiki-secret-ok 可豁免。與表格描述一致。

五支全部核對通過,沒有發現表格描述跟實際邏輯對不上的情況。


我看不懂、需要人看的地方

  • micromanage-guard.shirreversible-dispatch-guard.shno-ticket-no-dispatch.sh 的雙重矩比對(Task + Agent)是刻意保險還是舊名稱沒清掉——見上面「重複掛載」段落,我沒有把握單靠讀檔案判斷,需要看實際觸發紀錄或問總管。
  • stage-before-prod-guard.shmain-and-prod-push-guard.sh 的分工邊界main-and-prod-push-guard.sh 的檔頭明講自己是在「補 stage-before-prod-guard.sh 的破口」(那支只認 3 個關鍵字,抓不到 wrangler deploy),但兩支都還掛著、都還在管「prod 出貨」這件事。這是「新的補洞、舊的continua」還是「舊的該退休了」,這份盤點表不下判斷,留給你在下一步的分類會議裡定奪。

除了以上兩點,其餘 41 支的行為都能從檔頭與源碼直接讀出,沒有「猜」的部分。


這份表怎麼跟實況對帳(半年後怎麼發現漂移)

  1. 有沒有新閘沒被收進這張表:跑本文「落差偵測」段落的兩行 comm 指令,比對 hooks/*.sh 的檔名清單跟這張表列出的閘名清單(不是跟 hooks.json,那個只驗證有沒有註冊,驗不了有沒有寫進這張人話表)。
  2. 有沒有閘的行為跟這裡寫的不一樣了:抽幾支重新讀一次源碼,跟這裡的「一句話」對一遍——就是本文「抽驗 5 支」做的事,可以照同樣方法定期重做。
  3. .claude-plugin/plugin.jsonREADME.md 的數字:這兩處各自寫了一次「幾支、幾條註冊」,前面已經抓到一次對不上(42/52 vs 實際 43/53)。這兩個數字沒有機制保證跟著 hooks/ 目錄自動更新,是本表發現的第一個具體漂移案例。