/** * ╔══════════════════════════════════════════════════════════════════╗ * ║ Confirm Destructive Actions – Pi Extension ║ * ║ Version 1.1 (Bugfix: dd-Pattern, Befehlsanzeige) ║ * ╚══════════════════════════════════════════════════════════════════╝ * * ZWECK * ───── * Fängt destruktive Aktionen ab bevor Pi sie ausführt und fragt den * Nutzer um Erlaubnis. Kein Token-Verbrauch – läuft als Event-Hook * außerhalb des Kontextfensters. * * GESCHÜTZTE BEREICHE * ─────────────────── * 1. Dateilöschung rm, rmdir, find -delete, shred, trash, dd * 2. Git-Destruktion push --force, reset --hard, clean, branch -D, ... * 3. Cloud/AWS s3 rm, cloudformation delete, terraform destroy, ... * 4. Netzwerk-Exfiltration curl/wget Upload, scp, rsync remote, nc reverse shell * 5. Systembereich Schreibzugriff außerhalb ~ und Projektordner * 6. Datenbanken DROP, DELETE ohne WHERE, TRUNCATE, FLUSHALL * 7. SSH/Remote ssh mit destruktivem Befehl, ansible-playbook * 8. Shell-Konfiguration .bashrc, .zshrc, .ssh/config, crontab, sudoers * 9. Prozesse/Services kill -9, pkill, systemctl stop/disable * 10. Package-Installation npx unbekannte Pakete, npm install -g * 11. write-Tool Überschreiben bestehender Dateien + Secret-Scan * 12. edit-Tool Alle Dateiänderungen + Secret-Scan * * INSTALLATION * ──────────── * cp confirm-destructive.ts ~/.pi/agent/extensions/ * # Pi neu starten – Extension wird automatisch geladen * * Alte Version entfernen falls vorhanden: * rm ~/.pi/agent/extensions/confirm-deletion.ts * * CHANGELOG * ───────── * v1.1 – Bugfixes: * - FIX: dd-Pattern schlug fälschlicherweise auf /dev/null an * (z.B. bei `infisical secrets get ... 2>/dev/null`) * ALT: /\bdd\s+if=\/(dev|null)\b/ * NEU: /\bdd\b.*\bif=\/dev\/(zero|urandom|random)\b/ * - FIX: Befehl wurde nach 120 Zeichen abgeschnitten (jetzt 600) * - NEU: Mehrzeilige Befehle (\ am Zeilenende) werden korrekt angezeigt * - NEU: debugClassify() Export für lokales Debugging * * v1.0 – Erstversion mit allen 12 Schutzbereichen * * LOKALES DEBUGGING * ───────────────── * Wenn die Extension ein Falsch-Positiv produziert oder einen Befehl * nicht abfängt, kannst du sie mit einem lokalen Pi-Agenten debuggen: * * Methode A – direkt im Pi-Terminal (empfohlen): * ┌─────────────────────────────────────────────────────────────┐ * │ Schreibe in deinem Pi-Terminal: │ * │ │ * │ Ich möchte confirm-destructive.ts debuggen. │ * │ Teste ob dieser Befehl fälschlicherweise anschlägt: │ * │ TOKEN=$(infisical secrets get X --plain 2>/dev/null) │ * │ │ * │ Pi liest dann debugClassify() aus und testet den Befehl. │ * └─────────────────────────────────────────────────────────────┘ * * Methode B – Node.js direkt (ohne Pi): * ┌─────────────────────────────────────────────────────────────┐ * │ # TypeScript kompilieren: │ * │ npx tsc confirm-destructive.ts --module commonjs \ │ * │ --target es2020 --esModuleInterop │ * │ │ * │ # Debug-Skript erstellen (debug.mjs): │ * │ import { debugClassify } from "./confirm-destructive.js" │ * │ debugClassify('TOKEN=$(infisical secrets get X 2>/dev/null)') │ * │ debugClassify('git push --force origin main') │ * │ debugClassify('rm -rf ./dist') │ * │ │ * │ node debug.mjs │ * └─────────────────────────────────────────────────────────────┘ * * Methode C – Pattern isoliert testen (schnellste Methode): * ┌─────────────────────────────────────────────────────────────┐ * │ node -e " │ * │ const p = /\bdd\b.*\bif=\/dev\/(zero|urandom|random)\b/ │ * │ console.log(p.test('dd if=/dev/zero of=/dev/sda')) │ * │ console.log(p.test('infisical 2>/dev/null')) │ * │ " │ * └─────────────────────────────────────────────────────────────┘ * * NEUES PATTERN HINZUFÜGEN * ──────────────────────── * 1. Passendes Array suchen (z.B. GIT_DESTRUCTIVE_PATTERNS) * 2. Pattern hinzufügen mit Kommentar was es abfängt * 3. Mit Methode C oben testen: echte Treffer ja, Falsch-Positive nein * 4. Schweregrad in classifyBashRisk() prüfen (critical/high/medium) * 5. Version im Header hochzählen + Changelog-Eintrag * * AUSNAHME HINZUFÜGEN (Falsch-Positiv beheben) * ───────────────────────────────────────────── * Für Pfade: EXCLUDED_PATH_PATTERNS erweitern * Für Domains: TRUSTED_DOMAINS erweitern * Für Pakete: SAFE_PACKAGE_COMMANDS erweitern */ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import * as fs from "fs"; import * as path from "path"; // ═══════════════════════════════════════════════════════════ // KONFIGURATION – hier kannst du Ausnahmen anpassen // ═══════════════════════════════════════════════════════════ /** Pfade die OHNE Bestätigung verändert werden dürfen (Build-Artefakte) */ const EXCLUDED_PATH_PATTERNS: RegExp[] = [ /\/node_modules\/\.cache\//, /\/\.next\/cache\//, /\/dist\/.*\.(map|d\.ts)$/, /\/\.turbo\//, /\/coverage\//, /\/\.nyc_output\//, /\/tmp\//, /\/var\/folders\//, // macOS temp ]; /** Domains die als vertrauenswürdig gelten (kein Exfiltrations-Alarm) */ const TRUSTED_DOMAINS: RegExp[] = [ /github\.com/, /npmjs\.com/, /pypi\.org/, /registry\.yarnpkg\.com/, /api\.anthropic\.com/, /amazonaws\.com\/\S+\.(zip|tar|tgz)$/, // nur Downloads, kein Upload ]; /** Package-Manager-Patterns die als sicher gelten (bekannte Registries) */ const SAFE_PACKAGE_COMMANDS: RegExp[] = [ /^npm (install|i|ci|update)\b/, /^yarn (add|install|upgrade)\b/, /^pnpm (add|install|update)\b/, /^pip install\b/, /^pip3 install\b/, ]; // ═══════════════════════════════════════════════════════════ // PATTERN-DEFINITIONEN // ═══════════════════════════════════════════════════════════ const FILE_DELETION_PATTERNS: RegExp[] = [ /\brm\b/, /\brmdir\b/, /\bfind\b.*\b(-delete|-exec)\b/, /\btrash\b/, /\bshred\b/, /\bdd\b.*\bif=\/dev\/(zero|urandom|random)\b/, // FIX v1.1: nur echter dd-Wipe, nicht /dev/null ]; const GIT_DESTRUCTIVE_PATTERNS: RegExp[] = [ /\bgit\s+push\b.*(-f\b|--force\b|--force-with-lease\b)/, /\bgit\s+reset\s+--hard\b/, /\bgit\s+clean\s+-[fdx]+\b/, /\bgit\s+branch\s+-D\b/, /\bgit\s+stash\s+(drop|clear)\b/, /\bgit\s+tag\s+-d\b/, /\bgit\s+push\b.*--delete\b/, /\bgit\s+rebase\b.*(-i|--interactive)\b/, /\bgit\s+filter-branch\b/, /\bgit\s+push\b.*--mirror\b/, ]; const CLOUD_DESTRUCTIVE_PATTERNS: RegExp[] = [ // AWS S3 /\baws\s+s3\s+(rm|delete-object|delete-objects|rb)\b/, /\baws\s+s3\s+sync\b.*--delete\b/, // AWS CloudFormation / Lambda / EC2 / IAM / RDS /\baws\s+cloudformation\s+(delete-stack|delete-change-set)\b/, /\baws\s+lambda\s+delete-function\b/, /\baws\s+ec2\s+(terminate-instances|delete-security-group|delete-key-pair)\b/, /\baws\s+iam\s+(delete-user|delete-role|delete-policy|delete-access-key)\b/, /\baws\s+rds\s+(delete-db-instance|delete-db-cluster|delete-db-snapshot)\b/, // Terraform / Kubernetes / Docker /\bterraform\s+(destroy|apply\b.*-destroy)\b/, /\bkubectl\s+delete\b/, /\bdocker\s+(rm|rmi|volume\s+rm|system\s+prune|container\s+prune|network\s+rm)\b/, ]; const NETWORK_EXFILTRATION_PATTERNS: RegExp[] = [ // curl/wget die Dateien hochladen (nicht nur herunterladen) /\bcurl\b.*(-d\s*@|-F\s*['"]?\w+=@|--data-binary\s*@|--upload-file|-T\s)/, /\bcurl\b.*\|\s*(bash|sh|zsh|fish|sudo)\b/, /\bwget\b.*\|\s*(bash|sh|zsh|fish|sudo)\b/, // scp/rsync die Dateien nach außen senden /\bscp\b.*\s+\w+@[^:]+:/, // scp localfile user@remote: /\brsync\b.*\s+\w+@[^:]+:/, // rsync ... user@remote: /\brsync\b.*-[a-z]*r[a-z]*.*@/, // rsync mit remote // Netcat Reverse Shell / Datenexfiltration /\bnc\b.*(-e|-c)\s/, // nc -e /bin/bash /\bncat\b.*(-e|-c)\s/, // SSH Tunnel / Port Forwarding (kann Daten tunneln) /\bssh\b.*-[LRD]\s*\d+/, ]; const SYSTEM_PATH_PATTERNS: RegExp[] = [ /\/(etc|usr|bin|sbin|lib|lib64|boot|sys|proc)\//, /\/Library\/LaunchDaemons\//, // macOS system services /\/Library\/LaunchAgents\//, // macOS user agents (persistent) /~\/\.ssh\//, // SSH-Konfiguration /~\/\.bashrc|~\/\.zshrc|~\/\.profile|~\/\.bash_profile/, /~\/\.gitconfig/, /\/etc\/cron/, /\/etc\/sudoers/, /~\/\.aws\//, // AWS Credentials /~\/\.config\/gcloud\//, // GCloud Credentials ]; const DATABASE_DESTRUCTIVE_PATTERNS: RegExp[] = [ // SQL: DROP, TRUNCATE, DELETE ohne WHERE /\b(mysql|psql|sqlite3)\b.*(-e|--command)\s+['"]?\s*(DROP|TRUNCATE)/i, /\b(mysql|psql|sqlite3)\b.*(-e|--command)\s+['"]?\s*DELETE\s+FROM\s+\w+\s*['";\s]/i, // kein WHERE /\b(mysql|psql|sqlite3)\b.*(-e|--command)\s+['"]?\s*DELETE\s+FROM\s+\w+\s*$/i, // MongoDB /\bmongo\b.*--eval\s+['"]?\s*(db\.(drop|dropDatabase|getCollection))/i, // Redis /\bredis-cli\b.*(FLUSHALL|FLUSHDB|DEL\s+\*)/i, ]; const SSH_REMOTE_PATTERNS: RegExp[] = [ // ssh mit destruktivem Befehl /\bssh\b\s+\S+\s+['"]?.*(rm\s+-rf|DROP\s+DATABASE|DELETE\s+FROM|kubectl\s+delete|terraform\s+destroy)/i, // ansible mit destruktivem Modul /\bansible\b.*(-m\s+shell|-m\s+command|-m\s+raw)\b.*-a\s+['"]?.*(rm\s+-rf|DROP|DELETE)/i, /\bansible-playbook\b/, // jedes Playbook fragen – kann alles tun ]; const SHELL_CONFIG_PATTERNS: RegExp[] = [ />>\s*~?\/?(\.bashrc|\.zshrc|\.profile|\.bash_profile|\.bash_login)/, />>\s*~?\/?\.ssh\/(config|authorized_keys|known_hosts)/, /\bcrontab\s+-[re]\b/, /\bvisudo\b/, />\s*\/etc\/environment/, /\blaunchctl\s+(load|unload|submit)\b/, // macOS services /\bsystemctl\s+(enable|disable|mask)\b/, /\bchmod\s+(777|a\+[wx]|o\+[wx])\b/, // gefährliche Berechtigungen /\bchown\s+root\b/, ]; const PROCESS_KILL_PATTERNS: RegExp[] = [ /\bkill\s+-9\b/, /\bpkill\b/, /\bkillall\b/, /\bsystemctl\s+(stop|kill)\b/, /\bservice\s+\w+\s+stop\b/, /\blaunchctl\s+remove\b/, ]; const PACKAGE_INSTALL_PATTERNS: RegExp[] = [ /\bnpx\s+(?!--yes\s+create-)\S+/, // npx mit unbekanntem Paket /\bnpm\s+install\s+-g\b/, // globale npm-Installation /\bpip\s+install\b.*--user\b/, /\bcurl\b.*\|\s*(bash|sh)\b/, // classic curl-pipe /\bwget\b.*-O-.*\|\s*(bash|sh)\b/, ]; const SECRET_CONTENT_PATTERNS: RegExp[] = [ /\bAWS_SECRET_ACCESS_KEY\s*[=:]/i, /\bAWS_ACCESS_KEY_ID\s*[=:]/i, /\bSECRET_KEY\s*[=:]/i, /\bAPI_KEY\s*[=:]/i, /\bPRIVATE_KEY\s*[=:]/i, /\bACCESS_TOKEN\s*[=:]/i, /\bAUTH_TOKEN\s*[=:]/i, /\bDATABASE_PASSWORD\s*[=:]/i, /\bDB_PASS(WORD)?\s*[=:]/i, /\bGITHUB_TOKEN\s*[=:]/i, /\bNPM_TOKEN\s*[=:]/i, /\bANTHROPIC_API_KEY\s*[=:]/i, /-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----/, /\bghp_[A-Za-z0-9]{36}\b/, // GitHub PAT /\bsk-[A-Za-z0-9]{48}\b/, // OpenAI Key /\bAIza[A-Za-z0-9_-]{35}\b/, // Google API Key ]; // ═══════════════════════════════════════════════════════════ // TYPEN // ═══════════════════════════════════════════════════════════ interface RiskResult { category: string; warning: string; severity: "critical" | "high" | "medium"; } // ═══════════════════════════════════════════════════════════ // HILFSFUNKTIONEN // ═══════════════════════════════════════════════════════════ function normalizePath(targetPath: string, cwd: string): string { const expanded = targetPath.replace(/^~/, process.env.HOME ?? "~"); return (expanded.startsWith("/") ? expanded : path.join(cwd, expanded)).replace(/\/+$/, ""); } function isExcludedPath(targetPath: string, cwd: string): boolean { const abs = normalizePath(targetPath, cwd); return EXCLUDED_PATH_PATTERNS.some((p) => p.test(abs)); } function isTrustedDomain(command: string): boolean { return TRUSTED_DOMAINS.some((p) => p.test(command)); } function fileExists(filePath: string, cwd: string): boolean { try { return fs.existsSync(normalizePath(filePath, cwd ?? "")); } catch { return true; // im Zweifel: existiert } } function extractTarget(command: string): string | null { const matchers: RegExp[] = [ /\brm\s+(-[rfFRdv]*\s+)?["']?([^\s"';|&>]+)/, /\brmdir\s+(-p\s+)?["']?([^\s"';|&>]+)/, /\btrash\s+["']?([^\s"';|&>]+)/, /\bshred\s+(-[nuzfv]*\s+)?["']?([^\s"';|&>]+)/, /\baws\s+s3\s+\S+\s+(s3:\/\/[^\s]+)/, /\bscp\s+\S+\s+(\S+)/, /\brsync\s+.*\s+(\w+@[^:]+:\S*)/, ]; for (const p of matchers) { const m = command.match(p); if (m) return m[m.length - 1]; } return null; } function detectSecretInContent(content: string): string | null { for (const pattern of SECRET_CONTENT_PATTERNS) { const match = content.match(pattern); if (match) return match[0]; } return null; } function classifyBashRisk(command: string): RiskResult | null { // Pipe-to-shell (höchste Priorität) if (NETWORK_EXFILTRATION_PATTERNS.some((p) => p.test(command)) || /\b(curl|wget)\b.*\|\s*(bash|sh|zsh|fish|sudo)\b/.test(command)) { if (isTrustedDomain(command) && !/\|\s*(bash|sh|zsh|fish|sudo)\b/.test(command)) { return null; // Download von vertrauenswürdiger Domain ohne pipe → ok } const isReversShell = /\bnc\b.*(-e|-c)\s/.test(command); return { category: "🌐 Netzwerk-Exfiltration", warning: isReversShell ? "KRITISCH: Mögliche Reverse-Shell-Verbindung nach außen!" : "Dieser Befehl sendet Daten an einen externen Server oder führt heruntergeladenen Code aus!", severity: "critical", }; } // SSH Remote mit destruktivem Befehl if (SSH_REMOTE_PATTERNS.some((p) => p.test(command))) { return { category: "🖥️ SSH/Remote-Befehl", warning: "Dieser Befehl führt destruktive Operationen auf einem Remote-System aus!", severity: "critical", }; } // Systembereich-Schreibzugriff const systemWrite = /[>|]\s*\/?(etc|usr|bin|sbin|lib|boot)\//i.test(command) || /\btee\s+\/(etc|usr|bin|sbin)\//i.test(command) || /\bchmod\s+(777|a\+[wx]|o\+[wx])\b/.test(command); if (systemWrite) { return { category: "🔒 Systembereich-Zugriff", warning: "ACHTUNG: Dieser Befehl schreibt in geschützte Systembereiche!", severity: "critical", }; } // Shell-Konfiguration ändern if (SHELL_CONFIG_PATTERNS.some((p) => p.test(command))) { return { category: "⚙️ Shell/System-Konfiguration", warning: "Dieser Befehl ändert persistente Systemkonfiguration (Shell, SSH, Cron, Services).", severity: "high", }; } // Git-Destruktion if (GIT_DESTRUCTIVE_PATTERNS.some((p) => p.test(command))) { const isForce = /--force\b|-f\b|--mirror\b/.test(command); return { category: "🔀 Git-Destruktion", warning: isForce ? "ACHTUNG: Force-Push überschreibt den Remote-Stand unwiederbringlich!" : "Dieser Git-Befehl löscht oder überschreibt Daten.", severity: isForce ? "critical" : "high", }; } // Cloud/AWS-Ressourcen if (CLOUD_DESTRUCTIVE_PATTERNS.some((p) => p.test(command))) { const isTerraformDestroy = /terraform\s+destroy/.test(command); return { category: "☁️ Cloud-Ressource", warning: isTerraformDestroy ? "KRITISCH: terraform destroy löscht ALLE verwalteten Infrastruktur-Ressourcen!" : "Dieser Befehl löscht Cloud-Ressourcen (AWS/Terraform/Kubernetes/Docker).", severity: isTerraformDestroy ? "critical" : "high", }; } // Datenbankoperationen if (DATABASE_DESTRUCTIVE_PATTERNS.some((p) => p.test(command))) { return { category: "🗄️ Datenbank-Destruktion", warning: "Dieser Befehl löscht Datenbank-Daten oder -Strukturen (DROP/TRUNCATE/DELETE/FLUSH)!", severity: "critical", }; } // Dateilöschung if (FILE_DELETION_PATTERNS.some((p) => p.test(command))) { const isRecursive = /\brm\s+-[^-]*r/i.test(command); return { category: "🗑️ Dateilöschung", warning: isRecursive ? "ACHTUNG: Rekursive Löschung – kann nicht rückgängig gemacht werden!" : "Dieser Befehl löscht Dateien oder Verzeichnisse.", severity: isRecursive ? "high" : "medium", }; } // Prozesse/Services beenden if (PROCESS_KILL_PATTERNS.some((p) => p.test(command))) { return { category: "⛔ Prozess/Service beenden", warning: "Dieser Befehl beendet Prozesse oder Services.", severity: "medium", }; } // Package-Installation (nur npx unbekannt + globale npm) if (PACKAGE_INSTALL_PATTERNS.some((p) => p.test(command))) { if (SAFE_PACKAGE_COMMANDS.some((p) => p.test(command))) return null; return { category: "📦 Package-Installation", warning: "Dieses Package könnte Lifecycle-Scripts ausführen (postinstall).", severity: "medium", }; } return null; } function buildBashMessage(command: string, risk: RiskResult): string { const target = extractTarget(command); const severityIcon = risk.severity === "critical" ? "🚨" : risk.severity === "high" ? "⚠️ " : "ℹ️ "; // FIX v1.1: Befehl vollständig anzeigen (war 120, jetzt 600 Zeichen) // Mehrzeilige Befehle (\ am Zeilenende) werden eingerückt dargestellt const displayCommand = command.length > 600 ? command.slice(0, 597) + "..." : command; const formattedCommand = displayCommand .split(/\\\n/) .map((line, i) => i === 0 ? ` Befehl: ${line.trim()}` : ` ${line.trim()}`) .join("\n"); return [ `${severityIcon} ${risk.category}`, "", formattedCommand, target ? ` Ziel: ${target}` : "", "", risk.warning, ].filter(Boolean).join("\n"); } // ───────────────────────────────────────────── // DEBUG-HILFSFUNKTION // Für lokales Debugging: pi confirm-destructive.ts --debug // Zeigt welches Pattern auf welchen Befehl anschlägt. // Verwendung im Pi-Terminal: // /skill:debug-extension // Oder direkt: node -e "require('./confirm-destructive.ts')" (nach tsc) // ───────────────────────────────────────────── export function debugClassify(command: string): void { const result = classifyBashRisk(command); if (!result) { console.log(`✅ KEIN TREFFER: "${command}"`); return; } console.log(`${result.severity.toUpperCase()} – ${result.category}`); console.log(` Befehl: ${command}`); console.log(` Warnung: ${result.warning}`); const target = extractTarget(command); if (target) console.log(` Ziel: ${target}`); } // ═══════════════════════════════════════════════════════════ // EXTENSION // ═══════════════════════════════════════════════════════════ export default function (pi: ExtensionAPI) { pi.on("tool_call", async (event, ctx) => { // ── 1. BASH ────────────────────────────────────────────────────────────── if (event.toolName === "bash") { const command = (event.input as { command: string }).command; if (!command) return undefined; const risk = classifyBashRisk(command); if (!risk) return undefined; // Ausnahme: bekannte Build-Artefakt-Pfade const target = extractTarget(command); if (target && ctx.cwd && isExcludedPath(target, ctx.cwd)) return undefined; if (!ctx.hasUI) { return { block: true, reason: `Destruktiver Befehl blockiert (kein UI): ${command}` }; } const allowed = await ctx.ui.confirm(buildBashMessage(command, risk), "Erlauben?"); if (!allowed) return { block: true, reason: `Abgelehnt: ${risk.category}` }; return undefined; } // ── 2. WRITE ───────────────────────────────────────────────────────────── if (event.toolName === "write") { const input = event.input as { path?: string; content?: string }; const filePath = input.path; const content = input.content ?? ""; if (!filePath || !ctx.hasUI) return undefined; // Secret im Inhalt → immer blockieren const secretMatch = detectSecretInContent(content); if (secretMatch) { const confirmed = await ctx.ui.confirm( "🔑 Möglicher Secret/API-Key im Dateiinhalt", `Gefunden: "${secretMatch}"\nZieldatei: ${filePath}\n\nTrotzdem schreiben?`, ); if (!confirmed) return { block: true, reason: "Secret-Schreibvorgang abgelehnt" }; return undefined; } // Systembereich-Schreibzugriff const absPath = normalizePath(filePath, ctx.cwd ?? ""); if (SYSTEM_PATH_PATTERNS.some((p) => p.test(absPath))) { const confirmed = await ctx.ui.confirm( "🔒 Schreibzugriff in Systembereich", `Ziel: ${absPath}\n\nDiese Datei liegt außerhalb deines Projekts. Erlauben?`, ); if (!confirmed) return { block: true, reason: "Systembereich-Schreibzugriff abgelehnt" }; return undefined; } // Bestehende Datei überschreiben if (fileExists(filePath, ctx.cwd ?? "")) { const confirmed = await ctx.ui.confirm( "📝 Datei überschreiben", `${filePath}\n\nDiese Datei existiert bereits. Inhalt wird vollständig ersetzt. Erlauben?`, ); if (!confirmed) return { block: true, reason: "Überschreiben abgelehnt" }; } return undefined; } // ── 3. EDIT ────────────────────────────────────────────────────────────── if (event.toolName === "edit") { const input = event.input as { path?: string; edits?: Array<{ oldText?: string; newText?: string }> }; const filePath = input.path; const edits = input.edits ?? []; if (!filePath || !ctx.hasUI) return undefined; // Secret in neuem Inhalt for (const edit of edits) { const secretMatch = detectSecretInContent(edit.newText ?? ""); if (secretMatch) { const confirmed = await ctx.ui.confirm( "🔑 Möglicher Secret/API-Key in Änderung", `Gefunden: "${secretMatch}"\nDatei: ${filePath}\n\nTrotzdem schreiben?`, ); if (!confirmed) return { block: true, reason: "Secret-Edit abgelehnt" }; return undefined; } } // Systembereich const absPath = normalizePath(filePath, ctx.cwd ?? ""); if (SYSTEM_PATH_PATTERNS.some((p) => p.test(absPath))) { const confirmed = await ctx.ui.confirm( "🔒 Änderung in Systembereich", `${absPath}\n\nDiese Datei liegt außerhalb deines Projekts. Erlauben?`, ); if (!confirmed) return { block: true, reason: "Systembereich-Änderung abgelehnt" }; return undefined; } const confirmed = await ctx.ui.confirm( "✏️ Datei ändern", `${filePath}\n${edits.length} Änderung(en)\n\nErlauben?`, ); if (!confirmed) return { block: true, reason: "Änderung abgelehnt" }; return undefined; } return undefined; }); }