pi-system/extensions/confirm-deletion.ts

614 lines
26 KiB
TypeScript
Raw Normal View History

/**
*
* 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 <host> 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;
});
}