pi-system/extensions/confirm-deletion.ts
Raimund Bauer fb3daab33f feat/init: PiSystem Infrastruktur-Repo mit SubConfirm
Enthält alle Pi-Orchestrator-Infrastrukturkomponenten:
- bin/Sub* Skripte (SubAgenten, SubStatus, SubWatcher, SubConfirm)
- extensions/ (arbeitsweise-guard, confirm-deletion, etc.)
- memory/ (arbeitsweise, subagent-autocheck)
- agent/AGENTS.md mit SubConfirm-Reaktionslogik
- install.sh: deterministisches, idempotentes Setup für neue Maschinen

SubConfirm (neu): Stasis-Detektor der alle 30s tmux-Sessions prüft.
Bei unverändertem Output sendet er den vollständigen Pane-Inhalt
an die Alert-Datei — der Orchestrator beurteilt selbst ob Handlung nötig.
Kein Keyword-Matching.
2026-06-02 11:53:37 +02:00

613 lines
26 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* ╔══════════════════════════════════════════════════════════════════╗
* ║ 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;
});
}