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>
145 lines
9.7 KiB
Bash
Executable File
145 lines
9.7 KiB
Bash
Executable File
#!/bin/bash
|
||
# github-contact-guard.sh — GitHub 接觸保險(D20,2026-07-02)
|
||
# 背景:兩個帳號因高頻動作被 flag 拿不回(幾十顆星+issues 全損)。
|
||
# 設計:戰鬥機武器保險模式——平時所有 github.com 接觸一律機械擋下(exit 2),
|
||
# 只有 leo 親手跑 scripts/github-arm.sh 解鎖(限時),期間每次接觸自動留痕。
|
||
# 誠實限制(mindset §7):AI 技術上可自建 .github-armed 繞過;本 hook 的價值是
|
||
# 擋手滑+留審計軌跡,最後一段靠 D20 鐵律與自律。絕不聲稱「不可能繞過」。
|
||
# 2026-08-10 leo:以後一律匿名讀,寫才實名(詳見下方判定區塊的說明)。
|
||
#
|
||
# 🔴 另一個誠實限制(2026-08-09,arcrun-rag#27 查出的根因,別再花時間重查):
|
||
# 本 hook 只看得到「Claude Code 的 Bash 工具呼叫本身的指令字串」。
|
||
# 若某支程式(例如 products/arcrun-rag/installer/scripts/ship.mjs)自己用
|
||
# node:child_process 的 execFileSync/spawnSync 直接 spawn `git push`,
|
||
# 那個 push 是那支程式的**子行程**,不會產生新的 Bash 工具呼叫——本 hook
|
||
# 從頭到尾看不到它,不管下面的正則怎麼改都一樣(8/8 23:16 那次出貨的 push
|
||
# 就是這樣:hook 看到的指令字串只有 `node ship.mjs --target prod --confirm`,
|
||
# 裡面沒有 git/github.com 字面,於是判成「匿名讀」放行,連 D20 保險存不存在
|
||
# 都沒機會查,log 自然也不會有那一筆)。
|
||
# ⇒ 這種「碰 GitHub 的動作被包在別支程式裡」的情況,保險檢查與留痕**必須由
|
||
# 那支程式自己做**(見 ship.mjs 用的 installer/scripts/d20-guard.mjs),
|
||
# 不能指望改這支 hook 就能補到——這支 hook 天生看不見子行程。
|
||
|
||
INPUT=$(cat)
|
||
|
||
CMD=$(printf '%s' "$INPUT" | python3 -c "
|
||
import json,sys
|
||
try:
|
||
d = json.load(sys.stdin)
|
||
print(d.get('tool_input',{}).get('command',''))
|
||
except Exception:
|
||
print('')
|
||
" 2>/dev/null)
|
||
|
||
[ -z "$CMD" ] && exit 0
|
||
|
||
# 命中判定(D20 邊界,2026-07-05 leo 拍板——Facebook 比喻定調):
|
||
# GitHub 不在乎你「讀」(clone/fetch/抓 release,不管實名匿名、自己的還別人的——那是它原本的功能,
|
||
# 像 FB 不禁你讀貼文)。它 abuse-detect 的是「機器人一直改/寫」(高頻 push、Actions fan-out、API 寫)
|
||
# ——那才是害 richblack/uncle6 被 flag 的模式。**真正的線=讀 vs 寫,不是匿名 vs 實名。**
|
||
# 所以:所有「讀」一律放行(含帶認證 clone 自己的 repo);只擋「寫」與「gh 高頻 API」。
|
||
# 只擋以下兩型(寫入 / 會寫的 API):
|
||
# ① gh CLI 網路子指令(高頻 API,多會寫,且走你 token)② git 寫入動詞指向 github(push / remote add)
|
||
# 放行:git clone/fetch/pull/ls-remote(任何 repo,帶不帶認證都是讀)、curl/wget、go get/pip。
|
||
HIT=""
|
||
# ① gh CLI —— 高頻 API,全擋(讀寫混雜且走你 token,保守全擋;真要唯讀查詢個案 arm)
|
||
if printf '%s' "$CMD" | grep -qE '(^|[;&|(]|\s)gh\s+(api|repo|issue|pr|auth|search|release|run|workflow|gist|browse)\b'; then
|
||
HIT="gh CLI(高頻 API,走你的 token)"
|
||
# ② git 寫入動詞指向 github(push / remote add 為 push 鋪路)—— 寫入,擋
|
||
elif printf '%s' "$CMD" | grep -qiE 'git\s+(push|remote\s+add)([^|;&]*)(github\.com)'; then
|
||
HIT="git 寫入 → github(push/remote add)"
|
||
# ②b 🔴 2026-08-05 補漏:上面那條只認指令裡的 **github.com 字面**
|
||
# ⇒ `git push origin main`(remote 名指向 GitHub)**完全不會被攔**。
|
||
# 實撞:08-05 出貨那次 `git push origin main` 是「沒被偵測到」才過的,
|
||
# 不是靠保險放行——等於 D20 的武器閘有一個大洞,而它平常看起來很正常。
|
||
# ⇒ 用 remote 名去把 URL 解出來再判。
|
||
# ⚠️ 只有解出來**真的是 github** 才擋——Gitea 是真相源、要能自由推,不可誤傷。
|
||
# 解不出來(不在 git repo/remote 不存在)就放行:那種情況 git 自己也會失敗。
|
||
# ②c 🔴 2026-08-10 再補一個漏:`git -c http.<url>.extraheader=... push -u github main`
|
||
# (publish-github.sh 的寫法)command 字面裡**本來就看得到** github.com——
|
||
# 不必等 remote 名稱解析成功才判斷得出來,而 remote 名稱解析在某些 cwd 下會失敗
|
||
# (見檔頭「2026-08-10 leo 簡化」那段的實撞紀錄)。字面查得到就直接判定,
|
||
# 查不到才退回舊的「解 remote 名稱」那條路——兩條路都失手才會誤放行。
|
||
elif printf '%s' "$CMD" | grep -qiE '(^|[;&|(]|\s)git\s+([^|;&]*\s)?push(\s|$)'; then
|
||
# 🔴 總管 2026-08-10 收窄:原版寫成「整串裡有 github.com 就擋」,
|
||
# 於是**連 commit message 提到那個網址都會被擋**(我自己第一次要 commit 就撞到)。
|
||
# ⇒ 改成必須是「push 的目標」:github.com 要出現在 push 之後、且中間不跨命令分隔符。
|
||
# 誤擋的代價不是不方便——是它會逼人拆指令繞路,久了整道閘就沒人當真。
|
||
# 第二次收窄:只看「push 後面**第一個非旗標參數**」是不是 github 網址。
|
||
# 光要求「同一段命令內」還不夠——commit message 寫「push 到 https://github.com/…」
|
||
# 中間沒有 ;&| 一樣命中。真正的推送,網址必定緊接在 push 與旗標之後。
|
||
if printf '%s' "$CMD" | grep -qiE 'push(\s+-{1,2}[A-Za-z0-9=_-]+)*\s+(https?://|git@|ssh://)[^[:space:]]*github\.com'; then
|
||
HIT="git push → github(push 的目標字面就是 github.com)"
|
||
else
|
||
CWD=$(printf '%s' "$INPUT" | python3 -c "
|
||
import json,sys
|
||
try: print(json.load(sys.stdin).get('cwd','') or '')
|
||
except Exception: print('')
|
||
" 2>/dev/null)
|
||
[ -n "$CWD" ] || CWD=$(pwd)
|
||
# 取 push 後面第一個非 flag 的字當 remote 名;沒有就是 origin
|
||
RNAME=$(printf '%s' "$CMD" | sed -nE 's/.*git[[:space:]]+([^|;&]*[[:space:]])?push[[:space:]]+(-[^[:space:]]+[[:space:]]+)*([A-Za-z0-9._//-]+).*/\3/p' | head -1)
|
||
[ -n "$RNAME" ] || RNAME="origin"
|
||
RURL=$(git -C "$CWD" remote get-url "$RNAME" 2>/dev/null)
|
||
if printf '%s' "$RURL" | grep -qi 'github\.com'; then
|
||
HIT="git push → github(remote「$RNAME」指向 GitHub)"
|
||
fi
|
||
fi
|
||
fi
|
||
# 明確放行(全部是「讀」,零 flag 風險——GitHub 原本就允許):
|
||
# git clone/fetch/pull/ls-remote 任何 github repo(自己的/別人的、帶認證與否都是讀)、
|
||
# curl/wget 抓 release/raw/codeload、go get/pip 抓公開 module。
|
||
# ——讀不是 abuse,不在 D20 射程。唯一風險是「機器人一直寫」,已由 ①② 擋住。
|
||
#
|
||
# 2026-08-10 leo 簡化:讀不分匿名/實名,一律放行、不計數、無頻率閘(見檔頭說明)。
|
||
# 舊版這裡曾有一段「實名讀頻率閘」(URL 帶 token/讀自己 private repo → 每 4 小時放行 1 次),
|
||
# 08-10 真的誤傷過一次出貨(見檔頭)。leo 拍板拿掉:判準只剩「有沒有在寫」,讀就是讀。
|
||
#
|
||
# 🔴 總管 2026-08-10 補回一件(agent 原版把偵測整段刪了,我只拿掉「擋」,不拿掉「看得見」):
|
||
# leo 的規則是「**一律匿名讀**」⇒ 帶著憑證去讀,本身就已經偏離規則。
|
||
# hook 沒辦法強迫一條指令變匿名,但**可以留下痕跡**——
|
||
# 否則哪天 GitHub 又來說我們高頻活動,我們手上沒有任何紀錄可以自證讀了什麼。
|
||
# ⇒ 實名讀:**放行**(照 leo 的簡化,不擋、不限速),但**寫一行進接觸紀錄**。
|
||
if [ -z "$HIT" ]; then
|
||
if printf '%s' "$CMD" | grep -qiE '(https?://)[^[:space:]/@]+@[a-z0-9._-]*(github\.com|githubusercontent\.com|codeload\.github)'; then
|
||
_LOG="${CLAUDE_PROJECT_DIR:-$(pwd)}/system-dev/docs/3-specs/autonomy-dispatch/github-contact-log.md"
|
||
printf -- '- %s | 實名讀(URL 帶憑證,已放行)| %s\n' \
|
||
"$(date '+%Y-%m-%d %H:%M')" "$(printf '%s' "$CMD" | sed 's|//[^@]*@|//<憑證省略>@|g' | cut -c1-120)" \
|
||
>> "$_LOG" 2>/dev/null
|
||
fi
|
||
exit 0
|
||
fi
|
||
|
||
PROJ="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
||
ARM_FILE="$PROJ/.github-armed"
|
||
LOG_FILE="$PROJ/system-dev/docs/3-specs/autonomy-dispatch/github-contact-log.md"
|
||
|
||
if [ -f "$ARM_FILE" ]; then
|
||
EXPIRY=$(sed -n '1p' "$ARM_FILE")
|
||
MISSION=$(sed -n '2p' "$ARM_FILE")
|
||
NOW=$(date +%s)
|
||
if [ "$NOW" -le "${EXPIRY:-0}" ] 2>/dev/null; then
|
||
# 已解鎖 → 放行 + 留痕(BDA 戰損記錄)
|
||
SHORTCMD=$(printf '%s' "$CMD" | head -c 200 | tr '\n' ' ')
|
||
printf '| %s | %s | `%s` |\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$MISSION" "$SHORTCMD" >> "$LOG_FILE" 2>/dev/null
|
||
exit 0
|
||
else
|
||
rm -f "$ARM_FILE"
|
||
echo "❌ BLOCKED by D20 GitHub 接觸儀式" >&2
|
||
echo "違反項:保險已過期(武裝時效已過,自動回到保險狀態)" >&2
|
||
echo "正確做法:需要繼續接觸 GitHub → 請 leo 重新在終端機跑 scripts/github-arm.sh \"任務描述\"" >&2
|
||
exit 2
|
||
fi
|
||
fi
|
||
|
||
echo "❌ BLOCKED by D20 GitHub 接觸儀式(武器保險未解除)" >&2
|
||
echo "命中:${HIT}" >&2
|
||
echo "背景:兩個 GitHub 帳號因高頻動作被 flag 永久拿不回。平時一律只碰 Gitea。" >&2
|
||
echo "正確做法(發射流程):" >&2
|
||
echo " 1. 先自問:這件事 Gitea 做不到嗎?(issue/PR/push 內部協作 Gitea 全都能)" >&2
|
||
echo " 2. 真的非 GitHub 不可 → 向 leo(總部)請示:目標 repo、動作清單、預估請求次數、為何非它不可" >&2
|
||
echo " 3. leo 同意後由 leo 親手在終端機跑:scripts/github-arm.sh \"任務描述\" [分鐘,預設30]" >&2
|
||
echo " 4. 解鎖期間動作守 ROE:單 repo、≤5 次網路請求、禁批量/迴圈/Actions、間隔像人手" >&2
|
||
echo "參考:system-dev/docs/2-architecture/decisions/D20-github-contact-protocol.md" >&2
|
||
exit 2
|