修好一次性戳記恆擋:InkStoneCo 專案版 hooks 與 plugin 雙掛載

inkstone/ISEP#122:leo 三次要部署被 prod-write-guard.sh 恆擋,蓋了戳記
也放不了行。根因不在 prod-write-guard.sh 本身,而在
InkStoneCo/.claude/settings.json 還登記著一份 2026-08-20 立 ISEP plugin
之前的舊 hook,跟 plugin 自己的 hooks.json 同時掛在同一個事件上——每個
PreToolUse 都跑了兩次,一次性戳記被第一份消耗掉,第二份永遠看到空戳記。

實查(在這台機器的樹上實數):InkStoneCo 專案版登記著 38 支 hook,
34 支跟 plugin 完全同名同用途(純殘骸)、3 支是已退役機制
(claim-verify-police.sh/subagent-claim-worksheet.sh/sdd-guard.sh,
ISEP#60/#91 早已裁定退役但專案版沒跟著退)、1 支(kv-write-guard.sh)
是專案版有、plugin 當時沒有的真閘。

這次:
- kv-write-guard.sh 原樣搬進本 repo(B 組,PreToolUse Write|Edit|MultiEdit),
  補齊那個真的缺口,附 8 條迴歸測試
- 新增 duplicate-hook-registration-guard.sh(E 組,SessionStart):
  往後任何專案的 settings.json 又跟 plugin 長出同名登記,開場就點名,
  不必再靠「戳記莫名其妙失效」才發現,附 8 條迴歸測試
- 兩支新閘:61→63 支、84→86 條註冊(在自己的樹上實數,不是加減推)

InkStoneCo 那邊的清理(37 支殘骸從 settings.json 與 .claude/hooks/ 移除,
kv-write-guard.sh 保留至本次升版並 /plugin update 之後)另外commit,
不在本 repo 範圍內。

驗證:重演過修好前的雙掛載(蓋一次戳記→專案版先吃掉→plugin 版恆擋,
EXIT=2),也驗過修好後單次執行放行(EXIT=0)與不蓋戳記仍被擋(EXIT=2)。

待總管定版。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-02 00:24:32 +08:00
parent 35cc56dfc5
commit ee085c1854
7 changed files with 434 additions and 2 deletions
+49
View File
@@ -605,6 +605,55 @@ B 段拿這台機器真正的 `.env` 對帳——**`.env` 不在的機器(雲
> ```
> ⇒ 第二條正是黑名單會放過去的形狀。
### A27 — 一次性戳記真的能放行一次:重複掛載清乾淨了,也不會靜靜復發(`inkstone/ISEP#122`
```
bash hooks/tests/duplicate-hook-registration-guard.test.sh
bash hooks/tests/kv-write-guard.test.sh
```
**該看到**:兩支各自 `8 通過 / 0 失敗`。全程用假的 plugin root/專案目錄,
不碰真正的 `InkStoneCo/.claude/` 或這台機器裝著的 ISEP plugin。
**它們在守什麼**2026-09-01 實撞):總管三次要部署 uncle6.me,三次被
`prod-write-guard.sh` 恆擋——蓋了戳記、確認戳記還在,執行動作時照樣被擋,
再看戳記已經不見。根因:`InkStoneCo/.claude/settings.json` 還登記著一份
2026-08-20 立 ISEP plugin 之前的舊 hook(那時 `.claude/hooks/` 是唯一的家),
跟 plugin 自己的 `hooks.json` 同時掛在同一個 `PreToolUse` 事件上——每個動作
都跑了**兩次**,一次由專案版、一次由 plugin。多數閘只是白跑一次沒感覺,
但一次性戳記(蓋了就消耗掉)被第一份吃光,第二份永遠看到空戳記,於是恆擋。
**實查結果**(在這台機器的樹上實數,不是推算):`InkStoneCo/.claude/settings.json`
登記著 38 支專案版 hook——**34 支跟 plugin 完全同名同用途**(純殘骸)、
**3 支是已經退役的機制**`claim-verify-police.sh``subagent-claim-worksheet.sh`
`sdd-guard.sh``inkstone/ISEP#60``#91` 早已裁定退役,但專案版沒有跟著退),
**1 支(`kv-write-guard.sh`)是專案版有、plugin 當時沒有的真閘**2026-08-25
立,長效資料不准寫進 KV)。前 37 支已從 `InkStoneCo/.claude/settings.json`
`.claude/hooks/` 刪除;第 38 支原樣搬進本 repo(同一輪 commit,見 B 組)。
**失敗**
- `kv-write-guard.test.sh` 任一「該擋」紅 ⇒ 搬過來時邏輯改壞了,
KV 寫入的偵測失靈
- `kv-write-guard.test.sh` 任一「不該擋」紅 ⇒ 誤攔,這比漏擋嚴重——
正常的 `.get`/退休戰的刪除/`kv-ok` 豁免都要放得過
- `duplicate-hook-registration-guard.test.sh` 的「該吵」任一紅 ⇒
重複掛載又會靜靜發生,**下一次會是另一支帶戳記語意的閘恆擋,
而且要等 leo 自己撞到才會被發現**
- `duplicate-hook-registration-guard.test.sh` 的「不該吵」任一紅 ⇒
誤攔,每個 session 開場都吵一次沒問題的環境,最後被學會忽略
### A28 — 收工前的實測:蓋一次戳記真的只用得掉一次(手動,這台機器)
這一格驗的是**行為**,不是**檔案**——上面兩支測試證明「新機制自己邏輯對」,
這格證明「舊的重複掛載真的被拆掉了」。在**新開**的 session 裡跑(PreToolUse hook
是 session 啟動時載入的常駐清單,同一支閘改完不會讓已經在跑的 session 重新讀):
```
bash "$CLAUDE_PLUGIN_ROOT/scripts/gate-ok" prod-write # 蓋一次戳記
# 執行一個會被 prod-write-guard.sh 擋的動作(例如 wrangler pages deploy 打非白名單目標)
```
**該看到**:蓋戳記那個會被擋的動作**這次放行**(① 蓋一次戳記 → ② 動作通過);
不蓋戳記重跑同一個動作 → **仍然被擋**(證明沒有把閘弄鬆,只是不再被自己的複本吃掉)。
**失敗**:蓋了戳記仍被擋 ⇒ 還有第三份複本沒清乾淨,去跑
`bash "$CLAUDE_PLUGIN_ROOT/hooks/duplicate-hook-registration-guard.sh"`
(或直接開一個新 session 看它開場有沒有吵)先定位是哪一份。
### A5 — 開票前的搜尋是跨 repo 的
```
python3 scripts/ticket where 標籤 模組化
+21 -2
View File
@@ -1,4 +1,4 @@
# 61 支閘,白話盤點表
# 63 支閘,白話盤點表
> 回應 `inkstone/InkStoneCo#40`:「如果加入了,我應該可以白話文看到 hooks 的內容?」
> 這份表就是那個「白話文」——不用點開任何 `.sh` 檔,一行看懂一支閘在管什麼。
@@ -7,10 +7,27 @@
## 一句話結論
`hooks/` 底下有 **61 個 `.sh` 檔**`hooks.json` 實際掛上 **84 條註冊**(同一支閘常被多種情境同時掛上);
`hooks/` 底下有 **63 個 `.sh` 檔**`hooks.json` 實際掛上 **86 條註冊**(同一支閘常被多種情境同時掛上);
其中 **3 支檔案存在但沒被掛上**(2 支是待人填的空範本、1 支是刻意留著沒開的止血帶,見下面「未生效」表)。
下面按「你會在什麼時候撞到它」分組,一支一行。
> 📌 **`inkstone/ISEP#122`2026-09-02)進來兩支、+2 條**`kv-write-guard.sh`B 組,
> 2026-08-25 立於 InkStoneCo 專案版,原樣搬進來)與 `duplicate-hook-registration-guard.sh`
> (E 組,這次新寫)。61→**63** 支、84→**86** 條,兩個數字都是加完之後**在自己的樹上當場數出來的**
> `ls hooks/*.sh | wc -l``grep -c '"command":' hooks/hooks.json`),不是拿上一版加二。
>
> 這張票查的是「總管三次要部署被 `prod-write-guard.sh` 恆擋、蓋了戳記也放不了行」,
> 根因不在 `prod-write-guard.sh` 本身,而在 `InkStoneCo/.claude/settings.json`
> 還登記著一份跟 ISEP plugin 同名的舊 hook——每個 `PreToolUse` 事件都跑了**兩次**
> 一次性戳記被第一份消耗掉,第二份永遠看到空戳記、恆擋。實查:專案版登記著 38 支,
> **34 支跟 plugin 完全同名同用途**(純殘骸,已從 `InkStoneCo/.claude/settings.json`
> 刪除登記)、**3 支是已經退役的機制**`claim-verify-police.sh`
> `subagent-claim-worksheet.sh``sdd-guard.sh`,同樣刪除)、
> **1 支(`kv-write-guard.sh`)是專案版有、plugin 當時沒有的真閘**——原樣搬進本 repo,
> 這就是這次 B 組新增的那一支。`duplicate-hook-registration-guard.sh` 是防復發的機械閘:
> 下次任何專案的 `settings.json` 又長出跟 plugin 同名的登記,SessionStart 就會點名,
> 不必再靠「戳記莫名其妙失效」這種事後除錯才發現。
>
> 🔴 **這兩個數字上一版是錯的(2026-08-26 實際數過才發現)**:本頁原本寫「43 個檔、53 條註冊」,
> 而當時真實是 **45 個檔、55 條註冊**——中間有兩支閘進來時沒有回頭改這裡。
> 現在的寫法是實際數出來的:
@@ -145,6 +162,7 @@
| `mistake-needs-ticket-guard.sh` | AI 想往 `mistakes.md`(教訓紀錄)新增一條「機制可以防止」的教訓,卻沒附對應票號就擋下——沒有票號的教訓沒有人會回頭處理。 | 🛑 擋 |
| `wiki-size-guard.sh` | AI 想**一口氣砍掉 wiki 檔一大半內容**(淨縮水超過 800 字且超過原本 45%)就擋一次——那不是一次編輯,那是一次壓縮,而**壓縮會弄丟東西,弄丟的當下沒有人會發現**。出路是走 `scripts/wiki-compress`:它逼你附票號、把壓掉了什麼寫進 `.compress-log.md`,並用內文雜湊**逐條對帳**證明沒弄丟。只是改字、加字、小修一律不碰;真要手改就在內容裡放 `wiki-compress-ok` 留痕。 | 🛑 擋(至多攔一次) |
| `pending-changes-retired.sh` | AI 想寫東西進已經廢除的 `pending-changes.md` 檔案就擋下——這個檔案已停用,規格變更一律改開 Gitea 票。 | 🛑 擋 |
| `kv-write-guard.sh` | AI 想新增一行「往 KV 綁定寫入」的程式碼(`大寫識別字.put(``.delete(`)就擋下——長效資料一律要走 KBDB API,KV 只是暫存。判準是形狀(全大寫識別字 + `.put``.delete`),不是列名字清單;只讀(`.get``.list`)放行,刪除既有 KV 寫入(退休戰)也放行。真的是短命資料(session/nonce/有 TTL 的快取)在那一行尾端加 `kv-ok: <為什麼>` 留痕放行。2026-08-25 立於 InkStoneCo 專案版,`inkstone/ISEP#122` 原樣搬進本 repo。 | 🛑 擋 |
> 🔴 **`sdd-guard.sh` 已於 `inkstone/ISEP#91` 退役**(原本掛在這一組)。
> 它擋的是「動 code 檔時,`status: active` 的 SDD 不是恰好一份」。
@@ -209,6 +227,7 @@
| `overdue-nag-guard.sh` | 對話一開始就去 Gitea 撈**沒有人會叫的事**:逾期的 milestone、掛著等你的票(標「等了幾天」)、標著「有人在做」卻好幾天沒動的票;撈完用白話講出來,**有事就發 Telegram 給你**。沒東西可報時它會說「查過了,沒有」——**安靜跟壞掉長得一模一樣**。發不出去時不會靜默:它會先問這個 session 的 `prod-write-guard` 會不會擋(舊版把「發通知」誤認成「部署」),擋就改貼回票上並把原文印在眼前。只在開 session 時跑一次,**不輪詢、不掛排程**。 | 📝 記錄(context 注入 Telegram |
| `wiki-size-guard.sh` | 對話一開始講出「哪幾個 wiki 檔已經沒有人讀得完了」(預設超過 1200 行就點名)。**讀不完的必讀檔,跟沒有那個檔的差別只在於它讓人以為有。** 另一半掛在寫檔上,見 B 組。 | 📝 記錄(context 注入) |
| `skill-deploy-drift-guard.sh` | 如果「全機真正在用的 skill」跟「repo 裡版控的正本」內容對不上,就在開場講出來——避免用著一份沒人知道已經跟正本分家的舊拷貝。 | 📝 記錄 |
| `duplicate-hook-registration-guard.sh` | 如果這個專案自己的 `.claude/settings.json` 還登記著跟 ISEP plugin 同名的舊 hook,就在開場點名是哪幾支——**同一個動作跑兩次**,多數閘只是白跑,但帶「蓋一次戳記只放行一次」語意的閘會被第一份吃光,第二份永遠看到空戳記、恆擋(`inkstone/ISEP#122` 就是這樣,2026-09-01 撞了三次才被抓到)。判準是「有沒有真的註冊」,不是「檔案存不存在」。 | 📝 記錄(context 注入) |
## F. AI 想結束這一輪、要收工的時候(Stop)