c2638668e3
leo 2026-08-20:「同一個 plugin 你用,薄殼也用,保證兩邊同步」
「我要你幫雲端做薄殼,永遠都有問題,你要做的就是這組設定
你自己可以 dogfooding」
搬進來:41 支 hook(51 條註冊)/7 支 command/2 支 skill/23 支腳本。
不搬 .env、wiki、docs——那些是知識不是環境。
51 條 hook 路徑全部從 $CLAUDE_PROJECT_DIR/.claude/hooks/ 改成 ${CLAUDE_PLUGIN_ROOT}/hooks/,
零漏網。那正是薄殼一直壞掉的根:雲端 cwd 不是真身,寫死路徑就斷。
尚未驗證:Claude Code 能不能從私有 Gitea repo 裝 marketplace(要憑證)。
下一步就是在本機實際裝一次,通了才動雲端 bootstrap.sh。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
129 lines
6.0 KiB
Bash
Executable File
129 lines
6.0 KiB
Bash
Executable File
#!/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
|