diff --git a/.claude/hooks/awoooi-guard.js b/.claude/hooks/awoooi-guard.js new file mode 100644 index 00000000..5482ce27 --- /dev/null +++ b/.claude/hooks/awoooi-guard.js @@ -0,0 +1,129 @@ +// AWOOOI 專案守衛 hook — PreToolUse +// 阻擋生產環境高危操作,整合 pre-commit-check.sh 邏輯 + +let d = ''; +process.stdin.on('data', c => d += c); +process.stdin.on('end', () => { + try { + const i = JSON.parse(d); + const tool = i.tool_name || ''; + const cmd = String(i.tool_input?.command || ''); + const filepath = String(i.tool_input?.file_path || ''); + + // ── Bash 指令守衛 ────────────────────────────────────────── + if (tool === 'Bash') { + // git commit / git push 的 -m 或 heredoc 內容可能含任何關鍵字,跳過所有規則 + if (/git\s+commit|git\s+push/.test(cmd)) { process.stdout.write(d); return; } + + // 只在行首(或 && ; | 後)的真實命令才觸發,避免 commit message 誤觸 + const lines = cmd.split(/\n|&&|\|\||;/).map(s => s.trim()).filter(Boolean); + + // [HARD BLOCK] K8s 生產命名空間刪除 + if (lines.some(l => /^kubectl.*delete.*namespace.*awoooi-prod/.test(l))) { + process.stdout.write(JSON.stringify({ + decision: 'block', + reason: '🔴 [AWOOOI-GUARD] 禁止刪除生產命名空間 awoooi-prod' + })); + return; + } + + // [HARD BLOCK] K8s 生產環境強制刪除 PVC / Secret + if (lines.some(l => /^kubectl.*delete.*(pvc|secret).*-n.*awoooi-prod/.test(l) || + /^kubectl.*-n.*awoooi-prod.*delete.*(pvc|secret)/.test(l))) { + process.stdout.write(JSON.stringify({ + decision: 'block', + reason: '🔴 [AWOOOI-GUARD] 禁止在 awoooi-prod 刪除 PVC 或 Secret — 需人工確認' + })); + return; + } + + // [HARD BLOCK] docker compose down -v(摧毀 volume) + if (lines.some(l => /^docker[\s-]?compose.*down.*(-v\b|--volumes)/.test(l))) { + process.stdout.write(JSON.stringify({ + decision: 'block', + reason: '🔴 [AWOOOI-GUARD] 禁止 docker compose down -v — 會刪除資料庫 volume' + })); + return; + } + + // [HARD BLOCK] docker system prune(清除所有容器/映像) + if (lines.some(l => /^docker system prune/.test(l) && /-f|--force/.test(l))) { + process.stdout.write(JSON.stringify({ + decision: 'block', + reason: '🔴 [AWOOOI-GUARD] 禁止 docker system prune -f — 會清除 Gitea 等共用容器' + })); + return; + } + + // [HARD BLOCK] Telegram bot logout(先停後換原則)—— 只攔截實際 API 呼叫 + if (/api\.telegram\.org\/bot[^/]+\/(logOut|getUpdates|deleteWebhook)/.test(cmd)) { + process.stdout.write(JSON.stringify({ + decision: 'block', + reason: '🔴 [AWOOOI-GUARD] 禁止 Telegram logOut / getUpdates — 見 feedback_telegram_token_disaster.md' + })); + return; + } + + // [HARD BLOCK] 直接 DROP TABLE / DROP DATABASE(非測試環境) + if (lines.some(l => /^psql.*-c.*DROP\s+(TABLE|DATABASE|SCHEMA)/i.test(l)) && + !/test|dev|sqlite|memory/i.test(cmd)) { + process.stdout.write(JSON.stringify({ + decision: 'block', + reason: '🔴 [AWOOOI-GUARD] 禁止直接 DROP TABLE/DATABASE — 需先確認非生產環境' + })); + return; + } + + // [HARD BLOCK] git push --force 到 gitea main(在 git push 以外的脈絡才檢查) + if (lines.some(l => /^git push.*(--force|-f).*gitea.*main|^git push.*gitea.*main.*(--force|-f)/.test(l))) { + process.stdout.write(JSON.stringify({ + decision: 'block', + reason: '🔴 [AWOOOI-GUARD] 禁止 force push 到 gitea main' + })); + return; + } + + // [WARN] kubectl delete 在生產(非 PVC/Secret,允許但警告) + if (lines.some(l => /^kubectl.*delete.*-n.*awoooi-prod|^kubectl.*-n.*awoooi-prod.*delete/.test(l) && + !/(pvc|secret)/.test(l))) { + process.stderr.write('[AWOOOI-GUARD] ⚠️ 警告:在 awoooi-prod 執行 kubectl delete,請確認這是預期操作\n'); + } + + // [HARD BLOCK] 修改 Gitea runners(GitHub Billing 規則) + if (/ubuntu-latest/.test(cmd) && /workflow|\.github/.test(cmd)) { + process.stdout.write(JSON.stringify({ + decision: 'block', + reason: '🔴 [AWOOOI-GUARD] 禁止使用 ubuntu-latest — 必須用 self-hosted runner(費用)' + })); + return; + } + + } + + // ── Write/Edit 檔案守衛 ───────────────────────────────────── + if (tool === 'Write' || tool === 'Edit') { + // 保護 K8s namespace 定義不被意外改名 + if (/k8s.*prod|kubernetes.*prod|awoooi-prod/.test(filepath) && + /namespace.*awoooi/.test(String(i.tool_input?.old_string || '') + String(i.tool_input?.new_string || ''))) { + process.stderr.write('[AWOOOI-GUARD] ⚠️ 警告:修改生產 K8s namespace 定義,請確認變更範圍\n'); + } + + // 保護 CI/CD workflow 不引入 ubuntu-latest + if (/\.github\/workflows/.test(filepath)) { + const content = String(i.tool_input?.content || i.tool_input?.new_string || ''); + if (/runs-on:\s*ubuntu-latest/.test(content)) { + process.stdout.write(JSON.stringify({ + decision: 'block', + reason: '🔴 [AWOOOI-GUARD] 禁止在 workflow 使用 ubuntu-latest — 必須用 self-hosted(GitHub Billing)' + })); + return; + } + } + } + + } catch (e) { + // parse 失敗時放行,不阻斷正常操作 + } + + process.stdout.write(d); +});