#!/usr/bin/env bash # empty-handed-stop-guard.sh — 「空手停下」封路(leo 2026-08-17 立) # # ── 為什麼是這支,而不是再修一次句型偵測 ────────────────────────────── # leo 2026-08-17 的診斷(原話節錄): # 「你的 hook 為什麼漏?因為你在**文字層**封路。抓問號、抓句型,這就是軍火競賽: # 『回「做」我就啓動』沒有問號,下次它會寫『準備就緒』,再下次寫『待命中』。 # 自然語言的變體是無限的,blacklist 永遠追不完。 # 封路哲學之所以在 forge 架構裡有效,是因為它封的是**動作**(不能 push main、 # 不能繞過 PR),動作是有限且可枚舉的;文字不是。」 # # 當日實測佐證:文字層的閘**八次誤攔、零次正確攔截** # irreversible-dispatch-guard 4(三次咬的是「禁止」句本身) # stage-before-prod-guard 2(內文出現 bundle repo 名就當成在出貨) # browser-verify-guard 1(認不出另一套瀏覽器工具) # factory-idle-guard 1(剝引號把自己的句子攪碎) # ⇒ 而且方向穩定:**紅線寫得越細,命中關鍵字的機會越高 ⇒ 懲罰謹慎。** # # ── 這支封的是狀態,不是措辭 ──────────────────────────────────────── # 判準只有一條:**這個回合有沒有實際執行的證據(tool call)?** # 有 → 放行,不管文字怎麼寫 # 沒有 → 擋一次,注入「不等待確認,繼續執行」 # ⇒ 句型隨它變(「回 X 我就做」「準備就緒」「待命中」都一樣), # 但「空手停下」這條路本身物理上不通。 # # ── 迴圈安全 ────────────────────────────────────────────────────── # `stop_hook_active` 為真=已經擋過一次 ⇒ 放行。**至多提醒一次,不會卡死。** # # ── 留痕(InkStoneCo#48:36 支閘只有 2 支會記錄自己擋了什麼)─────────── # 擋下與放行都記。只記擋下的話分母未知,回答不了「這道閘有沒有在運作」。 INPUT=$(cat) LOG="$CLAUDE_PROJECT_DIR/.claude/hooks/empty-handed-stop-guard.log" VERDICT=$(printf '%s' "$INPUT" | python3 -c ' import json, os, sys try: d = json.load(sys.stdin) except Exception: print("SKIP"); raise SystemExit # 已經擋過一次 ⇒ 不再擋(迴圈安全) if d.get("stop_hook_active"): print("SKIP:already-nudged"); raise SystemExit tp = d.get("transcript_path") or "" if not tp or not os.path.exists(tp): print("SKIP:no-transcript"); raise SystemExit rows = [] try: with open(tp) as f: for line in f: line = line.strip() if line: try: rows.append(json.loads(line)) except Exception: pass except Exception: print("SKIP:unreadable"); raise SystemExit # 這個回合=最後一則「真的來自使用者」的訊息之後(工具結果不算) start = 0 for i, r in enumerate(rows): if r.get("type") == "user": c = (r.get("message") or {}).get("content") blocks = c if isinstance(c, list) else [{"type": "text"}] if not any(isinstance(b, dict) and b.get("type") == "tool_result" for b in blocks): start = i turn = rows[start:] # 數這個回合裡「我實際做了幾個動作」 tools = 0 for r in turn: if r.get("type") != "assistant": continue c = (r.get("message") or {}).get("content") if not isinstance(c, list): continue for b in c: if isinstance(b, dict) and b.get("type") == "tool_use": tools += 1 print(("EMPTY" if tools == 0 else "OK") + ":%d" % tools) ' 2>/dev/null || echo "SKIP:crash") TOOLS="${VERDICT##*:}" STAMP=$(date "+%Y-%m-%d %H:%M:%S" 2>/dev/null || echo "?") case "$VERDICT" in EMPTY:*) printf '| %s | ⛔ 擋下 | 這個回合 %s 個動作 |\n' "$STAMP" "$TOOLS" >> "$LOG" 2>/dev/null || true cat >&2 <<'MSG' 🤖 空手停下警察:**這個回合你一個動作都沒做。** 【leo 2026-08-17 的診斷】 「你的命令是第一把鑰匙(讓它開始想),它要再跟你要第二把(讓它開始做)。 從它的『文化』看,這是禮貌;**從你的系統看,這是違約。**」 ━━ 常駐授權(這不是禁令,是授權)━━━━━━━━━━━━━━━━━━━━━━━━ 🔑 **leo 的直接命令即完整授權。** 收到命令後停下來等第二次確認 = **任務失敗**,不是謹慎。 提問不是謹慎,是**把成本轉嫁給正在上課的人**。 ━━ 不確定時走這條,不要停 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ① 查 wiki(`system-dev/wiki/`,語意搜尋優先於 grep) ② 套四題公式(花錢/不可逆/跨專案結構/品味方向) ③ 仍不確定 → **做出最合理的假設,把假設寫進 commit message 或票的留言,繼續走** 🔴 **假設之後不是問,是記錄。** leo 下課後 review 時一次看到所有假設:對的併,錯的打回, 錯的連同「為什麼不妥」寫回 wiki ⇒ 下次同類情境查得到判例。 ⇒ 這把**同步的提問**改造成**非同步的問答**——問題排隊等他,不掛起整個 loop。 ━━ 真的無事可做?━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 本閘**至多提醒一次**(`stop_hook_active` 之後放行),不會卡死。 若這回合確實只是回答一個不需要任何動作的問題,直接再送一次即可。 MSG exit 2 ;; OK:*) printf '| %s | ✅ 放行 | 這個回合 %s 個動作 |\n' "$STAMP" "$TOOLS" >> "$LOG" 2>/dev/null || true exit 0 ;; *) printf '| %s | ⚪ 略過 | %s |\n' "$STAMP" "$VERDICT" >> "$LOG" 2>/dev/null || true exit 0 ;; esac