/** * Secret Scanner Extension * * WHAT: Blocks `write`/`edit` tool calls whose content looks like a secret * (API keys, tokens, private keys, credentials). Complements * protected-paths (which blocks by path) by scanning content. * * CONFIGURE: edit SECRET_PATTERNS in extensions/lib/secrets.ts. * * DISABLE: remove/rename this file from the package `extensions/` folder. */ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { scanSecrets } from "./lib/secrets.ts"; import { audit } from "./lib/audit.ts"; export default function secretScannerExtension(pi: ExtensionAPI) { pi.on("tool_call", async (event, ctx) => { if (event.toolName !== "write" && event.toolName !== "edit") return undefined; // Scan every string field of the input except the path (content, new_string, // etc.) so we don't depend on a specific tool's field naming. const input = event.input as Record; const parts: string[] = []; for (const [key, value] of Object.entries(input)) { if (key === "path") continue; if (typeof value === "string") parts.push(value); } const hits = scanSecrets(parts.join("\n")); if (hits.length === 0) return undefined; const path = typeof input.path === "string" ? input.path : "(unknown)"; audit({ kind: "guardrail_block", guardrail: "secret-scanner", tool: event.toolName, path, patterns: hits }); if (ctx.hasUI) { const choice = await ctx.ui.select( `⚠️ Похоже на секрет (${hits.join(", ")}) в записи → ${path}\nВсё равно записать?`, ["Нет", "Да"], ); if (choice === "Да") return undefined; } return { block: true, reason: `Possible secret in content (${hits.join(", ")})` }; }); }