Plugin: sd0x-dev-flow 4.1.0(marketplace sd0xdev-marketplace)
影響: 所有「同時把 hook 安裝到 .claude/hooks/ 且註冊進 .claude/settings.json」的專案(也就是跑過 /install-hooks 或 /project-setup 的標準安裝)
症狀
Auto-loop 完全靜默失效,但沒有任何錯誤訊息:
.claude_review_state.json 停在 session-init 寫入的初始值,updated_at 整個 session 不再變動
code_review / doc_review / precommit 三個 receipt 永遠 executed: false
session_commit_scope.touched_files 永遠是 []
iteration_history.current_round 永遠 0,即使實際跑了十幾輪 review
/next-step 因此持續回報 P0 gate-missing-doc,且無論跑幾次 /codex-review-doc 都不會清除
根因
7 支 hook 開頭都有這個區塊(以 post-tool-review-state.sh:7-31 為例,其餘位置見下表):
# === Plugin-defers-to-local arbitration ===
_SELF_NAME="$(basename "$0")"
if [[ -n "${CLAUDE_PROJECT_DIR:-}" ]] \
&& [[ ! -f "${CLAUDE_PROJECT_DIR}/hooks/hooks.json" ]] \
&& [[ -x "${CLAUDE_PROJECT_DIR}/.claude/hooks/${_SELF_NAME}" ]]; then
_SETTINGS_MATCH=false
for _sf in "${CLAUDE_PROJECT_DIR}/.claude/settings.json" \
"${CLAUDE_PROJECT_DIR}/.claude/settings.local.json"; do
... # grep / jq 找 ".claude/hooks/${_SELF_NAME}"
done
if [[ "$_SETTINGS_MATCH" == "true" ]]; then
exit 0 # Defer to local hook
fi
fi
條件判斷的是「本地那份存在、且被註冊」,從來沒有比對執行中的 $0 是不是就是那份本地檔案。basename "$0" 只用來取檔名,不用來辨識身分。
因此當本地副本被 settings.json 觸發執行時,它同樣通過三個條件(本地檔存在 ✓、可執行 ✓、settings 有註冊 ✓),於是 defer 給自己並 exit 0。plugin 副本被 hooks.json 觸發時也 defer。兩份都退讓,沒有任何一份真正執行。
註解寫的是 "avoid double-fire",實際行為是 zero-fire。
重現
專案已跑過 /install-hooks(.claude/hooks/*.sh 存在且可執行、.claude/settings.json 有對應註冊):
P=~/.claude/plugins/cache/sd0xdev-marketplace/sd0x-dev-flow/4.1.0
cat > /tmp/payload.json <<'EOF'
{"session_id":"test","tool_name":"mcp__codex__codex-reply","tool_input":{"prompt":"x"},
"tool_response":{"content":[{"type":"text","text":"## Document Review\n\nprobe\n"}]}}
EOF
# A. 設了 CLAUDE_PROJECT_DIR(= 真實 session 的樣子)
CLAUDE_PROJECT_DIR="$PWD" bash "$P/hooks/post-tool-review-state.sh" < /tmp/payload.json
# → 無輸出,exit 0(靜默 defer)
# B. 不設 CLAUDE_PROJECT_DIR(仲裁條件第一項不成立,直接略過)
bash "$P/hooks/post-tool-review-state.sh" < /tmp/payload.json
# → [Review State] MCP output carries a '## Document Review' header but ...(hook 真的跑了)
本地那份 .claude/hooks/post-tool-review-state.sh 與 plugin 這份 hash 相同,行為完全一致。
旁證:session-init.sh 是 8 支裡唯一沒有這個仲裁區塊的,也是唯一實際有效運作的一支——.claude_review_state.json 之所以還有初始內容,就是它寫的。
受影響檔案(4.1.0)
| Hook |
仲裁區塊起始行 |
hooks/post-tool-review-state.sh |
7 |
hooks/post-compact-auto-loop.sh |
9 |
hooks/user-prompt-review-guard.sh |
9 |
hooks/stop-guard.sh |
13 |
hooks/post-skill-auto-loop.sh |
14 |
hooks/pre-edit-guard.sh |
16 |
hooks/post-edit-format.sh |
17 |
hooks/session-init.sh |
—(無此區塊,未受影響) |
7 支全部使用 _SELF_NAME="$(basename "$0")",沒有一支比對路徑。
影響評估
嚴重度高於表面症狀:這是 fail-open。auto-loop.md § Enforcement 的設計是「hook 無法持久記錄轉換時,寫 fail-closed sidecar marker 並讓 stop-guard 使該 gate 失效」——但這個 bug 讓 hook 根本不執行,連 fail-closed 路徑都走不到。STOP_GUARD_MODE=strict 也不會擋,因為 stop-guard 本身同樣 defer 掉了。
結果是:使用者以為 gate 有 hook 在強制執行,實際上完全沒有,且沒有任何訊號。
建議修法
仲裁應該比對解析後的路徑,只有在「我不是本地那份」時才 defer:
_SELF_NAME="$(basename "$0")"
_SELF_DIR="$(cd "$(dirname "$0")" 2>/dev/null && pwd -P)" || _SELF_DIR=""
_LOCAL_DIR="$(cd "${CLAUDE_PROJECT_DIR}/.claude/hooks" 2>/dev/null && pwd -P)" || _LOCAL_DIR=""
if [[ -n "${CLAUDE_PROJECT_DIR:-}" ]] \
&& [[ ! -f "${CLAUDE_PROJECT_DIR}/hooks/hooks.json" ]] \
&& [[ -n "$_SELF_DIR" && -n "$_LOCAL_DIR" ]] \
&& [[ "$_SELF_DIR" != "$_LOCAL_DIR" ]] \
&& [[ -x "${_LOCAL_DIR}/${_SELF_NAME}" ]]; then
... # 既有的 settings 比對與 defer
fi
pwd -P 解析 symlink,避免 .claude 是連結時誤判。_SELF_DIR 取不到時(極少數情況)保守選擇不 defer——寧可 double-fire 也不要 zero-fire,因為 double-fire 至少會被看見。
另建議加一道健檢:/claude-health 或 /install-hooks 驗證階段實際觸發一次 hook,斷言 .claude_review_state.json 的 updated_at 有變動。純粹比對「檔案存在 + 已註冊」無法偵測這個 bug——本專案兩項都通過,但 hook 一次都沒跑。
暫時繞道
移除 .claude/settings.json 的 hooks 註冊(保留 .claude/hooks/*.sh 檔案本身),讓 plugin 自己的 hooks/hooks.json 驅動。_SETTINGS_MATCH 因此為 false,plugin 副本正常執行。已實測有效。
代價:之後再跑 /install-hooks 會把註冊加回來,bug 重現。
Plugin:
sd0x-dev-flow4.1.0(marketplacesd0xdev-marketplace)影響: 所有「同時把 hook 安裝到
.claude/hooks/且註冊進.claude/settings.json」的專案(也就是跑過/install-hooks或/project-setup的標準安裝)症狀
Auto-loop 完全靜默失效,但沒有任何錯誤訊息:
.claude_review_state.json停在 session-init 寫入的初始值,updated_at整個 session 不再變動code_review/doc_review/precommit三個 receipt 永遠executed: falsesession_commit_scope.touched_files永遠是[]iteration_history.current_round永遠0,即使實際跑了十幾輪 review/next-step因此持續回報 P0gate-missing-doc,且無論跑幾次/codex-review-doc都不會清除根因
7 支 hook 開頭都有這個區塊(以
post-tool-review-state.sh:7-31為例,其餘位置見下表):條件判斷的是「本地那份存在、且被註冊」,從來沒有比對執行中的
$0是不是就是那份本地檔案。basename "$0"只用來取檔名,不用來辨識身分。因此當本地副本被
settings.json觸發執行時,它同樣通過三個條件(本地檔存在 ✓、可執行 ✓、settings 有註冊 ✓),於是 defer 給自己並exit 0。plugin 副本被hooks.json觸發時也 defer。兩份都退讓,沒有任何一份真正執行。註解寫的是 "avoid double-fire",實際行為是 zero-fire。
重現
專案已跑過
/install-hooks(.claude/hooks/*.sh存在且可執行、.claude/settings.json有對應註冊):本地那份
.claude/hooks/post-tool-review-state.sh與 plugin 這份 hash 相同,行為完全一致。旁證:
session-init.sh是 8 支裡唯一沒有這個仲裁區塊的,也是唯一實際有效運作的一支——.claude_review_state.json之所以還有初始內容,就是它寫的。受影響檔案(4.1.0)
hooks/post-tool-review-state.shhooks/post-compact-auto-loop.shhooks/user-prompt-review-guard.shhooks/stop-guard.shhooks/post-skill-auto-loop.shhooks/pre-edit-guard.shhooks/post-edit-format.shhooks/session-init.sh7 支全部使用
_SELF_NAME="$(basename "$0")",沒有一支比對路徑。影響評估
嚴重度高於表面症狀:這是 fail-open。
auto-loop.md§ Enforcement 的設計是「hook 無法持久記錄轉換時,寫 fail-closed sidecar marker 並讓 stop-guard 使該 gate 失效」——但這個 bug 讓 hook 根本不執行,連 fail-closed 路徑都走不到。STOP_GUARD_MODE=strict也不會擋,因為 stop-guard 本身同樣 defer 掉了。結果是:使用者以為 gate 有 hook 在強制執行,實際上完全沒有,且沒有任何訊號。
建議修法
仲裁應該比對解析後的路徑,只有在「我不是本地那份」時才 defer:
pwd -P解析 symlink,避免.claude是連結時誤判。_SELF_DIR取不到時(極少數情況)保守選擇不 defer——寧可 double-fire 也不要 zero-fire,因為 double-fire 至少會被看見。另建議加一道健檢:
/claude-health或/install-hooks驗證階段實際觸發一次 hook,斷言.claude_review_state.json的updated_at有變動。純粹比對「檔案存在 + 已註冊」無法偵測這個 bug——本專案兩項都通過,但 hook 一次都沒跑。暫時繞道
移除
.claude/settings.json的 hooks 註冊(保留.claude/hooks/*.sh檔案本身),讓 plugin 自己的hooks/hooks.json驅動。_SETTINGS_MATCH因此為 false,plugin 副本正常執行。已實測有效。代價:之後再跑
/install-hooks會把註冊加回來,bug 重現。