#!/bin/bash # .claude/hooks/pre-write-guard.sh # arcrun PreToolUse guard for Write / Edit / MultiEdit # # 職責:擋下會違反 CLAUDE rules 的檔案寫入操作 # 退出 code: # 0 = 允許 # 2 = 擋下(stderr 訊息會回傳給 CC) # # 依賴:jq set -o pipefail INPUT=$(cat) FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // ""') # 取得將要寫入的內容(Write: content;Edit: new_string;MultiEdit: edits[].new_string 全部串起來) CONTENT=$(echo "$INPUT" | jq -r ' .tool_input.content // .tool_input.new_string // (.tool_input.edits // [] | map(.new_string // "") | join("\n")) // "" ') block() { local rule="$1" local reason="$2" local fix="$3" cat >&2 </" \ "改去 registry/components/auth_static_key/ 等目錄,用 TinyGo 實作 main.go" fi fi # ───────────────────────────────────────────────────────────────────────────── # 規則 2.1:禁止新增含特定關鍵字的 TS 檔案(credential-injector / jwt-signer 等) # ───────────────────────────────────────────────────────────────────────────── if [[ "$FILE_PATH" == *.ts ]]; then BASE=$(basename "$FILE_PATH") # 既有的 credential-injector.ts / jwt-signer.ts 允許修改(為了刪除),但不准新增同名 if [[ "$BASE" =~ ^(credential[-_]injector|jwt[-_]signer)\.ts$ ]]; then if [[ ! -f "$FILE_PATH" ]]; then block "2.1" \ "禁止新增 ${BASE}(Phase 1-3 的目標是刪除此類檔案,不是重建)" \ "credential 注入 / JWT signing 屬於 WASM 零件職責,改去 registry/components/auth_*/" fi fi fi # ───────────────────────────────────────────────────────────────────────────── # 規則 2.2:cypher-executor TS 裡不准實作業務邏輯(只准 wasi-shim.ts 做 crypto) # ───────────────────────────────────────────────────────────────────────────── if [[ "$FILE_PATH" == *"cypher-executor/src/"* && "$FILE_PATH" == *.ts ]]; then BASE=$(basename "$FILE_PATH") # crypto.subtle.decrypt:只准在 wasi-shim.ts if echo "$CONTENT" | grep -qE "crypto\.subtle\.decrypt"; then if [[ "$BASE" != "wasi-shim.ts" ]]; then block "2.2" \ "AES-GCM 解密(crypto.subtle.decrypt)只准出現在 wasi-shim.ts 的 crypto_decrypt host function" \ "把解密邏輯移到 wasi-shim.ts 的 host function;或讓 WASM 零件透過 u6u.crypto_decrypt 呼叫" fi fi # crypto.subtle.sign with RSASSA:只准在 wasi-shim.ts if echo "$CONTENT" | grep -qE "crypto\.subtle\.sign.*RSASSA"; then if [[ "$BASE" != "wasi-shim.ts" ]]; then block "2.2" \ "RS256 簽章只准出現在 wasi-shim.ts 的 crypto_sign_rs256 host function" \ "把簽章移到 wasi-shim.ts;或讓 auth_service_account WASM 透過 u6u.crypto_sign_rs256 呼叫" fi fi # Template 展開:{{secret.X}} 或 {{runtime.X}} 屬於 WASM 職責 # 例外:auth-recipe-seeds.ts 是 recipe 資料定義(會被序列化寫進 RECIPES KV), # 其中的 {{secret.X}} / {{runtime.X}} 是「資料字面值」而非 TS 展開邏輯, # 真正的展開仍在 WASM auth primitive 內完成。 if [[ "$BASE" != "auth-recipe-seeds.ts" ]] && echo "$CONTENT" | grep -qE "\{\{(secret|runtime)\." ; then block "2.2" \ "Template 展開({{secret.X}} / {{runtime.X}})屬於 WASM auth primitive 職責" \ "把這段邏輯改寫到 registry/components/auth_static_key/main.go(TinyGo)" fi # Hard-code 的 BUILTIN_API_RECIPES / BUILTIN_CREDENTIALS_MAP 新增 if echo "$CONTENT" | grep -qE "(BUILTIN_API_RECIPES|BUILTIN_CREDENTIALS_MAP)\s*[:=]"; then # 允許「把它設成空物件」或「刪除」,但不准新增實作 if echo "$CONTENT" | grep -qE "BUILTIN_API_RECIPES.*=.*\{\s*[a-zA-Z]"; then block "2.2" \ "禁止在 TS 裡新增 BUILTIN_API_RECIPES / BUILTIN_CREDENTIALS_MAP 實作" \ "API 呼叫邏輯屬於各自的 WASM 零件(gmail.wasm / telegram.wasm 等),cypher-executor 只做 routing" fi fi # Hard-code API endpoint 實作 HARDCODED_APIS=( "gmail\.googleapis\.com/gmail/v1/users/me/messages/send" "api\.telegram\.org/bot.*sendMessage" "sheets\.googleapis\.com/v4/spreadsheets" "notify-api\.line\.me/api/notify" ) for PATTERN in "${HARDCODED_APIS[@]}"; do if echo "$CONTENT" | grep -qE "$PATTERN"; then # 允許 wasi-shim.ts 裡的 http_request host function(它只是 proxy) if [[ "$BASE" != "wasi-shim.ts" ]]; then block "2.2" \ "禁止在 cypher-executor TS 裡 hard-code API endpoint(偵測到: $PATTERN)" \ "把 API 呼叫移到對應的 WASM 零件(registry/components/gmail/main.go 等)" fi fi done # exchangeGoogleJwt / 類似 token exchange function if echo "$CONTENT" | grep -qE "(exchangeGoogleJwt|exchangeServiceAccountJwt|signGoogleJwt)"; then if [[ "$BASE" != "wasi-shim.ts" ]]; then block "2.2" \ "Token exchange 邏輯屬於 auth_service_account WASM 零件" \ "改到 registry/components/auth_service_account/main.go" fi fi fi # ───────────────────────────────────────────────────────────────────────────── # 規則 3.3:禁止建立 *-v2 / new-* / *-worker 類複製貼上目錄 # ───────────────────────────────────────────────────────────────────────────── if [[ "$FILE_PATH" =~ /(auth|credential|jwt|oauth|gmail|telegram|google-sheets|line-notify|http-request)[-_](v2|v3|new|worker|backup|temp)/ ]]; then block "3.3" \ "禁止為同一零件建立平行目錄(v2/new/worker/backup 等)" \ "直接修改 registry/components//main.go 即可;需要版本管理請用 git branch" fi if [[ "$FILE_PATH" =~ /new-(auth|credential|jwt|oauth|gmail|telegram)/ ]]; then block "3.3" \ "禁止為同一零件建立 new-/ 平行目錄" \ "直接修改 registry/components//main.go" fi # ───────────────────────────────────────────────────────────────────────────── # 規則 4.3:禁止自行在 .agents/specs/ 下建新 SDD 目錄 # ───────────────────────────────────────────────────────────────────────────── if [[ "$FILE_PATH" == *".agents/specs/"* ]]; then # 檢查是否在已知 SDD 目錄內 KNOWN_SDDS=( ".agents/specs/arcrun" ".agents/specs/u6u-core-mvp" ".agents/specs/u6u-platform-evolution" ".agents/specs/component-registry-canon" ".agents/specs/component-gatekeeping" # 2026-05-29 richblack 確認新建(Phase 3 把關) ) IN_KNOWN=false for K in "${KNOWN_SDDS[@]}"; do if [[ "$FILE_PATH" == *"$K/"* ]]; then IN_KNOWN=true break fi done if [[ "$IN_KNOWN" == "false" ]]; then block "4.3" \ "禁止自行在 .agents/specs/ 下建立新的頂層 SDD 目錄" \ "先與 richblack 確認 SDD 範圍。若是現有 SDD 的補充檔案,請放到已知 SDD 目錄下" fi fi exit 0