ee085c1854
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>
69 lines
3.7 KiB
Bash
Executable File
69 lines
3.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# duplicate-hook-registration-guard.sh — SessionStart:這個專案自己的 .claude/settings.json
|
||
# 是不是還在登記一支跟 ISEP plugin 同名的舊 hook(inkstone/ISEP#122)
|
||
#
|
||
# 為什麼存在(2026-09-01 實撞):
|
||
# 總管三次要部署 uncle6.me,三次被 prod-write-guard.sh 擋。leo 問「你沒有權限?」
|
||
# ——不是沒權限,是 InkStoneCo/.claude/settings.json 裡還留著 ISEP 立 plugin 之前
|
||
# 的舊登記,跟 plugin 自己的 hooks.json 同時掛在同一個事件上,於是每個 PreToolUse
|
||
# 都跑了**兩次**:一次由專案版、一次由 plugin。多數閘只是白跑一次沒感覺,
|
||
# 但**帶「蓋一次戳記只放行一次」語意的閘會被第一份吃光**——第二份永遠看到空的戳記,
|
||
# 於是恆擋。查證見 ISEP#122:37 支專案版登記裡,34 支跟 plugin 完全同名同用途、
|
||
# 3 支是已經退役的機制(`claim-verify-police.sh`/`subagent-claim-worksheet.sh`/
|
||
# `sdd-guard.sh`),全部清掉;剩下 1 支(`kv-write-guard.sh`)是專案版有、plugin
|
||
# 當時沒有的真閘,已原樣搬進本 repo(同一輪 commit)。
|
||
#
|
||
# 🔑 這支 hook 的用途不是清除,是**讓「又長出重複掛載」這件事有人立刻知道**。
|
||
# 上一次它能存活 13 天沒被發現,就是因為除了「戳記莫名其妙失效」以外沒有任何
|
||
# 訊號會指出來——一支天天在跑的閘,跟一支不存在的閘,行為長得一模一樣。
|
||
#
|
||
# 三條設計約束(跟 skill-deploy-drift-guard.sh 同一個形狀):
|
||
# 1. 沒事不出聲——乾淨時完全靜音。
|
||
# 2. 從不擋人——SessionStart 一律 exit 0,這是通知,不是閘。
|
||
# 3. 判準是「登記在不在」,不是關鍵字——比對的是 hooks.json/settings.json
|
||
# 裡實際出現的檔名,不是靠猜或列黑名單(isep-hand 紅線:不准用關鍵字黑名單)。
|
||
|
||
set -uo pipefail
|
||
|
||
ROOT="${CLAUDE_PLUGIN_ROOT:-}"
|
||
[ -n "$ROOT" ] || exit 0
|
||
[ -f "$ROOT/hooks/hooks.json" ] || exit 0
|
||
|
||
PROJ="${CLAUDE_PROJECT_DIR:-}"
|
||
[ -n "$PROJ" ] || exit 0
|
||
SETTINGS="$PROJ/.claude/settings.json"
|
||
[ -f "$SETTINGS" ] || exit 0
|
||
|
||
# ISEP plugin 自己「真的註冊了」的 hook 檔名(不是目錄列表——目錄裡可能有沒掛的樣板)
|
||
PLUGIN_NAMES="$(grep -oE 'hooks/[a-zA-Z0-9._-]+\.sh' "$ROOT/hooks/hooks.json" 2>/dev/null \
|
||
| sed 's|.*/||' | sort -u)"
|
||
[ -n "$PLUGIN_NAMES" ] || exit 0
|
||
|
||
# 這個專案自己的 settings.json 裡,指向 .claude/hooks/ 底下的登記
|
||
PROJECT_NAMES="$(grep -oE '\.claude/hooks/[a-zA-Z0-9_.-]+\.sh' "$SETTINGS" 2>/dev/null \
|
||
| sed 's|.*/||' | sort -u)"
|
||
[ -n "$PROJECT_NAMES" ] || exit 0
|
||
|
||
DUPES="$(comm -12 <(printf '%s\n' "$PLUGIN_NAMES") <(printf '%s\n' "$PROJECT_NAMES") 2>/dev/null || true)"
|
||
[ -n "$DUPES" ] || exit 0
|
||
|
||
N=$(printf '%s\n' "$DUPES" | grep -c .)
|
||
LIST="$(printf '%s\n' "$DUPES" | sed 's/^/ • /')"
|
||
|
||
cat <<EOF
|
||
🔁 重複掛載警報:\`$SETTINGS\` 還登記著 ${N} 支跟 ISEP plugin 同名的舊 hook。
|
||
|
||
$LIST
|
||
|
||
⇒ 每個對應的動作都會**跑兩次**——一次由這份專案版複本、一次由 plugin。
|
||
多數閘只是白跑一次;**帶「蓋一次戳記只放行一次」語意的閘會被第一份吃光**,
|
||
第二份永遠看到空戳記,於是恆擋——這正是 inkstone/ISEP#122 那次的形狀。
|
||
|
||
出路(二選一,不是「留著沒關係」):
|
||
① 這幾支 plugin 已經覆蓋 → 從 \`$SETTINGS\` 的 hooks 區段刪掉這幾條登記。
|
||
② 專案版身上還有 plugin 沒有的邏輯 → 先把那段邏輯併進 ISEP(開票、比照
|
||
kv-write-guard.sh 那次的搬法),plugin 升版裝上之後再刪專案版登記。
|
||
**不要讓兩份同時掛著**——那正是這支閘存在的理由。
|
||
EOF
|
||
exit 0
|