#!/usr/bin/env node /** * ARIA claude-CLI PreToolUse-Hook: blockiert Bash-Calls gegen externe APIs * fuer die bereits ein matching Skill im Brain existiert. * * Wird von claude-CLI PRO Tool-Use vor der Ausfuehrung mit dem Tool-Use- * JSON via stdin aufgerufen. Wenn wir exit 2 mit Stderr returnen, lehnt * claude-CLI den Tool-Call ab und gibt die Stderr als tool_use_error * an das LLM zurueck — ARIA bekommt also eine echte Fehlermeldung und * MUSS umdenken (nicht nur Prompt-Anweisung die sie ignorieren kann). * * Fail-open: bei jeder Art von Fehler (Brain nicht erreichbar, kaputtes * JSON etc.) exit 0 — wir blockieren Stefan's eigentliche Arbeit nicht * nur weil der Block-Mechanismus selber haengt. */ const http = require("http"); const BRAIN_URL = process.env.BRAIN_INTERNAL_URL || "http://aria-brain:8080"; const BRAIN_TIMEOUT_MS = 3000; function fail_open(reason) { if (process.env.HOOK_DEBUG) console.error(`hook-skip: ${reason}`); process.exit(0); } function block(message) { // exit 2 = block in claude-CLI PreToolUse hook contract process.stderr.write(message); process.exit(2); } let stdinBuf = ""; process.stdin.on("data", chunk => stdinBuf += chunk); process.stdin.on("end", () => { let payload; try { payload = JSON.parse(stdinBuf || "{}"); } catch (_) { return fail_open("stdin not json"); } // claude-CLI Hook-Format kann je nach Version variieren — // wir akzeptieren tool_name oder hook_event_name in Kombination const toolName = payload.tool_name || payload.tool || ""; if (toolName !== "Bash") return fail_open("tool != Bash"); const command = (payload.tool_input && payload.tool_input.command) || payload.command || ""; if (!command) return fail_open("no command"); // Schnellfilter: nur wenn ueberhaupt eine URL drin ist if (!/https?:\/\//i.test(command)) return fail_open("no url"); // Brain fragen ob ein matching Skill existiert const body = JSON.stringify({ command }); const req = http.request( BRAIN_URL + "/skills/can-bash-host", { method: "POST", headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body), }, timeout: BRAIN_TIMEOUT_MS, }, (res) => { let chunks = ""; res.on("data", d => chunks += d); res.on("end", () => { let r; try { r = JSON.parse(chunks); } catch (_) { return fail_open("brain bad json"); } if (!r || !r.block) return fail_open("brain says ok"); const skill = r.skill || "?"; const host = r.host || "?"; const safeTool = r.safe_tool || `run_${skill}`; const msg = `🚨 BASH GEGEN ${host} BLOCKIERT.\n\n` + `Es existiert bereits ein Skill '${skill}' fuer diesen Host. ` + `Stefan hat das System so eingerichtet dass Skills via ` + `\`${safeTool}\` direkt aufgerufen werden — das ist 5-10x ` + `schneller als der Bash-Curl-Wrapper.\n\n` + `Konkret: nutze JETZT \`${safeTool}\` mit den passenden ` + `Parametern (method/path/body) statt curl. Wenn der Skill ` + `nicht das liefert was Du brauchst: skill_update mit Fix, ` + `nicht zurueck zu Bash.`; block(msg); }); } ); req.on("error", () => fail_open("brain network error")); req.on("timeout", () => { req.destroy(); fail_open("brain timeout"); }); req.write(body); req.end(); }); // Falls stdin nie ein 'end' triggert — Timeout damit wir nicht haengen setTimeout(() => fail_open("stdin timeout"), 4000);