Files
ISEP/hooks/github-contact-guard.sh
T
Leo c2638668e3 ISEP 0.1.0:環境設定收成一個 plugin,本機與雲端共用一份
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>
2026-08-20 11:41:46 +08:00

145 lines
9.7 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# github-contact-guard.sh — GitHub 接觸保險(D202026-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-09arcrun-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 寫入動詞指向 githubpush / 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 寫入動詞指向 githubpush / remote add 為 push 鋪路)—— 寫入,擋
elif printf '%s' "$CMD" | grep -qiE 'git\s+(push|remote\s+add)([^|;&]*)(github\.com)'; then
HIT="git 寫入 → githubpush/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 → githubpush 的目標字面就是 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 → githubremote「$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