feat(claude): 新增 awoooi-guard.js 守衛 hook
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
129
.claude/hooks/awoooi-guard.js
Normal file
129
.claude/hooks/awoooi-guard.js
Normal file
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user