#!/usr/bin/env node /** * beforeShellExecution hook — turn the kit's "explicit authorization before * destructive or external action" rule into something that actually stops. * * A rule in a prompt is advice the model can rationalize past under momentum. * This is a gate. It never denies on its own — it returns "ask", so you decide, * with the reason named. That keeps the failure mode "one extra confirmation" * rather than "the agent cannot work". * * Contract: stdin is the beforeShellExecution payload; stdout is * {"permission": "allow"|"ask"|"deny", "user_message": "...", "agent_message": "..."}. * Exit 0 = success. This hook has no failClosed flag in hooks.json, so if node is * missing or this script throws, Cursor fails open and the session keeps working. */ // Routine `git push` is deliberately NOT gated: a prompt that fires on every push gets // approved reflexively and stops meaning anything. Force pushes are. Add your own. const PATTERNS = [ [/\brm\s+(-[a-zA-Z]*[rf][a-zA-Z]*\s+)+/, "recursive or forced delete"], [/\bgit\s+push\b.*(--force|-f)\b/, "force push"], [/\bgit\s+(reset\s+--hard|clean\s+-[a-zA-Z]*f)/, "discards uncommitted work"], [/\b(drop|truncate)\s+(table|database|schema)\b/i, "destructive database statement"], [/\b(migrate|db:migrate|alembic\s+upgrade|prisma\s+migrate\s+deploy)\b/i, "database migration"], [/\b(terraform|pulumi)\s+(apply|destroy)\b/, "infrastructure change"], [/\bkubectl\s+(delete|apply)\b/, "cluster change"], [/\b(npm|pnpm|yarn)\s+publish\b/, "package publish"], [/\b(vercel|netlify|fly|heroku|wrangler)\s+(deploy|publish)\b/i, "deployment"], [/\bdocker\s+(push|system\s+prune)\b/, "registry push or prune"], [/\bchmod\s+(-R\s+)?777\b/, "world-writable permissions"], [/\bcurl\b[^|]*\|\s*(ba)?sh\b/, "pipe-to-shell from the network"], [/>\s*\/dev\/sd[a-z]|\bmkfs\b|\bdd\s+if=.*of=\/dev\//, "raw device write"], ]; const readAll = () => new Promise((resolve) => { let data = ""; process.stdin.setEncoding("utf8"); process.stdin.on("data", (c) => (data += c)); process.stdin.on("end", () => resolve(data)); setTimeout(() => resolve(data), 2000).unref(); }); const allow = () => { process.stdout.write(JSON.stringify({ permission: "allow" })); process.exit(0); }; const raw = await readAll(); let command = ""; try { command = JSON.parse(raw || "{}").command || ""; } catch { allow(); } const hit = PATTERNS.find(([re]) => re.test(command)); if (!hit) allow(); const reason = hit[1]; process.stdout.write( JSON.stringify({ permission: "ask", user_message: `Gated: ${reason}. Approve only if you intended this.`, agent_message: `This command was gated as a ${reason}. Per the quality gates in AGENTS.md, destructive, external, ` + `and irreversible actions need explicit owner authorization — loop momentum is not authorization. ` + `If the owner declines, record it as a decision-ready item in docs/PROGRESS.md and re-route to another ` + `independent unit rather than looking for a way around this command.`, }), ); process.exit(0);