Commit ab1079cd420d

Vincent Demeester <vincent@sbr.pm>
2026-02-20 06:28:16
refactor(pi): merge nix-guardrails into guardrails
Merged nix-guardrails extension into unified guardrails with per-command scoped approvals (turn/session). Removed duplicate nixos-rebuild and home-manager rules. Fixed intercepted-commands shebangs for NixOS compatibility (#!/bin/bash → #!/usr/bin/env bash).
1 parent 4b3ec86
Changed files (13)
dots/pi/agent/extensions/guardrails/index.ts
@@ -4,9 +4,11 @@
  * Combines path validation policies with command security:
  * - Configurable path policies (match patterns, allowed/blocked paths)
  * - Dangerous command detection (rm -rf, sudo, mkfs, dd, fork bombs, ...)
+ * - Nix-specific guardrails (nix eval, nix-build, flake updates, etc.)
  * - Protected path writes (via bash redirects, cp, mv, tee)
  * - Soft-protected lockfiles (confirm before modifying)
- * - Actions: warn, block, or suggest redirect
+ * - Per-command scoped approvals (once, per-turn, per-session)
+ * - Actions: warn, block, confirm, or suggest redirect
  *
  * Policy config: ~/.config/ai/path-policies.json
  */
@@ -39,20 +41,46 @@ interface PolicyConfig {
 	policies: PathPolicy[];
 }
 
-// ── Dangerous command patterns ────────────────────────────────
+// ── Command rule types ────────────────────────────────────────
 
-const dangerousCommands = [
-	{ pattern: /\brm\s+(-[^\s]*r|--recursive)/, desc: "recursive delete" },
-	{ pattern: /\bsudo\b/, desc: "sudo command" },
-	{ pattern: /\b(chmod|chown)\b.*777/, desc: "dangerous permissions" },
-	{ pattern: /\bmkfs\b/, desc: "filesystem format" },
-	{ pattern: /\bdd\b.*\bof=\/dev\//, desc: "raw device write" },
-	{ pattern: />\s*\/dev\/sd[a-z]/, desc: "raw device overwrite" },
-	{ pattern: /\bkill\s+-9\s+-1\b/, desc: "kill all processes" },
-	{ pattern: /:\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;/, desc: "fork bomb" },
-	{ pattern: /\bnixos-rebuild\s+(switch|boot|test)/, desc: "direct nixos-rebuild (use make targets)" },
-	{ pattern: /\bhome-manager\s+switch\b/, desc: "direct home-manager switch (use make targets)" },
-	{ pattern: /\bkubectl\b/, desc: "kubectl command" },
+interface CommandRule {
+	pattern: RegExp;
+	desc: string;
+	action: "confirm" | "block";
+	suggestion?: string;
+}
+
+// ── Dangerous & guarded command patterns ──────────────────────
+
+const commandRules: CommandRule[] = [
+	// General dangerous commands (confirm)
+	{ pattern: /\brm\s+(-[^\s]*r|--recursive)/, desc: "recursive delete", action: "confirm" },
+	{ pattern: /\bsudo\b/, desc: "sudo command", action: "confirm" },
+	{ pattern: /\b(chmod|chown)\b.*777/, desc: "dangerous permissions", action: "confirm" },
+	{ pattern: /\bmkfs\b/, desc: "filesystem format", action: "confirm" },
+	{ pattern: /\bdd\b.*\bof=\/dev\//, desc: "raw device write", action: "confirm" },
+	{ pattern: />\s*\/dev\/sd[a-z]/, desc: "raw device overwrite", action: "confirm" },
+	{ pattern: /\bkill\s+-9\s+-1\b/, desc: "kill all processes", action: "confirm" },
+	{ pattern: /:\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;/, desc: "fork bomb", action: "confirm" },
+	{ pattern: /\bkubectl\b/, desc: "kubectl command", action: "confirm" },
+
+	// Nix commands (block: must use make targets)
+	{ pattern: /\bnixos-rebuild\s+(switch|boot|test)/, desc: "direct nixos-rebuild", action: "block", suggestion: "Use 'make switch', 'make boot', or 'make host/<hostname>/switch' instead" },
+	{ pattern: /\bhome-manager\s+switch\b/, desc: "direct home-manager switch", action: "block", suggestion: "Use 'make switch' or appropriate make target instead" },
+
+	// Nix commands (confirm)
+	{ pattern: /\bnix\s+eval\b/, desc: "nix eval (arbitrary code execution)", action: "confirm" },
+	{ pattern: /\bnix-build\b/, desc: "nix-build (builds derivations)", action: "confirm" },
+	{ pattern: /\bnix\s+store\s+(delete|gc|optimise)\b/, desc: "nix store modification", action: "confirm" },
+	{ pattern: /\bnix-collect-garbage\b/, desc: "nix garbage collection", action: "confirm" },
+	{ pattern: /\bnix-channel\s+(--add|--remove|--update)\b/, desc: "nix-channel modification", action: "confirm" },
+	{ pattern: /\bnix\s+flake\s+update\b/, desc: "nix flake update", action: "confirm" },
+	{ pattern: /\bnix\s+flake\s+lock\b/, desc: "nix flake lock", action: "confirm" },
+	{ pattern: /\bnix\s+(copy|push)\b/, desc: "nix store transfer", action: "confirm" },
+	{ pattern: /\bnix\s+(develop|shell)\b/, desc: "nix development shell", action: "confirm" },
+	{ pattern: /\bnix\s+run\b/, desc: "nix run (execute package)", action: "confirm" },
+	{ pattern: /\bnix-env\s+-[ie]/, desc: "nix-env package installation", action: "confirm", suggestion: "Consider using home-manager or system configuration instead" },
+	{ pattern: /\bnix\s+profile\s+(install|remove|upgrade)\b/, desc: "nix profile modification", action: "confirm", suggestion: "Consider using home-manager or system configuration instead" },
 ];
 
 // ── Protected path patterns (hard block via bash) ─────────────
@@ -325,26 +353,40 @@ export default function (pi: ExtensionAPI) {
 			const command = event.input.command;
 			const commandForMatching = stripQuotedContent(command);
 
-			// Check dangerous commands (confirm before allowing)
-			for (const { pattern, desc } of dangerousCommands) {
-				if (pattern.test(commandForMatching)) {
-					// Skip prompt if already approved for this scope
-					if (isApproved(desc)) break;
+			// Check command rules (block or confirm before allowing)
+			for (const rule of commandRules) {
+				if (rule.pattern.test(commandForMatching)) {
+					// Hard block: no approval possible
+					if (rule.action === "block") {
+						const reason = rule.suggestion
+							? `${rule.desc}: ${rule.suggestion}`
+							: rule.desc;
+						ctx.ui.notify(`🚫 BLOCKED: ${reason}`, "error");
+						return { block: true, reason };
+					}
+
+					// Confirm: skip prompt if already approved for this scope
+					if (isApproved(rule.desc)) break;
 
 					if (!ctx.hasUI) {
-						return { block: true, reason: `Blocked ${desc} (no UI to confirm)` };
+						return { block: true, reason: `Blocked ${rule.desc} (no UI to confirm)` };
 					}
+
+					const prompt = rule.suggestion
+						? `⚠️ ${rule.desc}\n💡 ${rule.suggestion}\n\n  ${command}\n\nAllow?`
+						: `⚠️ Dangerous command: ${rule.desc}\n\n  ${command}\n\nAllow?`;
+
 					const choice = await ctx.ui.select(
-						`⚠️ Dangerous command: ${desc}\n\n  ${command}\n\nAllow?`,
+						prompt,
 						["Yes", "Yes, for this turn", "Yes, for this session", "No"],
 					);
 					if (!choice || choice === "No") {
-						return { block: true, reason: `Blocked ${desc} by user` };
+						return { block: true, reason: `Blocked ${rule.desc} by user` };
 					}
 					if (choice === "Yes, for this turn") {
-						approvedCommands.push({ desc, scope: "turn" });
+						approvedCommands.push({ desc: rule.desc, scope: "turn" });
 					} else if (choice === "Yes, for this session") {
-						approvedCommands.push({ desc, scope: "session" });
+						approvedCommands.push({ desc: rule.desc, scope: "session" });
 					}
 					break;
 				}
dots/pi/agent/extensions/intercepted-commands/conda
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
 # Conda interception - only block package management commands
 # Allow conda for non-Python tasks if needed
 
dots/pi/agent/extensions/intercepted-commands/pip
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
 echo "Error: pip is disabled. Use uv instead:" >&2
 echo "" >&2
 echo "  To install a package for a script: uv run --with PACKAGE python script.py" >&2
dots/pi/agent/extensions/intercepted-commands/pip3
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
 echo "Error: pip3 is disabled. Use uv instead:" >&2
 echo "" >&2
 echo "  To install a package for a script: uv run --with PACKAGE python script.py" >&2
dots/pi/agent/extensions/intercepted-commands/pipenv
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
 echo "Error: pipenv is disabled. Use uv instead:" >&2
 echo "" >&2
 echo "  pipenv install   -> uv add / uv sync" >&2
dots/pi/agent/extensions/intercepted-commands/poetry
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
 echo "Error: poetry is disabled. Use uv instead:" >&2
 echo "" >&2
 echo "  poetry init      -> uv init" >&2
dots/pi/agent/extensions/intercepted-commands/python
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
 
 # Check for disallowed module invocations
 for arg in "$@"; do
dots/pi/agent/extensions/intercepted-commands/python3
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
 
 # Check for disallowed module invocations
 for arg in "$@"; do
dots/pi/agent/extensions/intercepted-commands/virtualenv
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
 echo "Error: virtualenv is disabled. Use uv instead:" >&2
 echo "" >&2
 echo "  virtualenv .venv -> uv venv" >&2
dots/pi/agent/extensions/nix-guardrails/bun.lock
@@ -1,19 +0,0 @@
-{
-  "lockfileVersion": 1,
-  "configVersion": 0,
-  "workspaces": {
-    "": {
-      "name": "nix-guardrails",
-      "devDependencies": {
-        "bun-types": "^1.0.0",
-      },
-    },
-  },
-  "packages": {
-    "@types/node": ["@types/node@25.2.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ=="],
-
-    "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="],
-
-    "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
-  }
-}
dots/pi/agent/extensions/nix-guardrails/index.ts
@@ -1,387 +0,0 @@
-/**
- * Nix Guardrails Extension for Pi
- *
- * Requires approval for potentially dangerous or impactful Nix commands:
- * - nix eval (can execute arbitrary code)
- * - nix-build (builds derivations)
- * - nixos-rebuild (direct system changes - should use make targets)
- * - home-manager switch (direct home changes - should use make targets)
- * - nix store operations (store modifications)
- * - nix-channel operations (channel management)
- * - nix-collect-garbage (garbage collection)
- * - nix flake update (flake lock updates)
- * - nix copy/push (store transfers)
- * - nix develop/shell (enters new shell environments)
- *
- * Configuration: ~/.config/ai/nix-guardrails.json
- */
-
-import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
-import { existsSync } from "node:fs";
-import { readFile, writeFile } from "node:fs/promises";
-import { join } from "node:path";
-import { homedir } from "node:os";
-
-// ── Policy types ──────────────────────────────────────────────
-
-interface NixCommandRule {
-	pattern: RegExp;
-	description: string;
-	action: "confirm" | "block" | "allow";
-	reason: string;
-	enabled: boolean;
-	// If true, show the full command in confirmation
-	showCommand?: boolean;
-	// Suggested alternative (e.g., "use make switch instead")
-	suggestion?: string;
-}
-
-interface NixGuardrailsConfig {
-	rules: Array<{
-		pattern: string;
-		description: string;
-		action: "confirm" | "block" | "allow";
-		reason: string;
-		enabled?: boolean;
-		showCommand?: boolean;
-		suggestion?: string;
-	}>;
-}
-
-// ── Default Nix command rules ─────────────────────────────────
-
-const DEFAULT_RULES: NixCommandRule[] = [
-	{
-		pattern: /\bnix\s+eval\b/,
-		description: "nix eval (arbitrary code execution)",
-		action: "confirm",
-		reason: "nix eval can execute arbitrary Nix code and has side effects",
-		enabled: true,
-		showCommand: true,
-	},
-	{
-		pattern: /\bnix-build\b/,
-		description: "nix-build (builds derivations)",
-		action: "confirm",
-		reason: "nix-build will build derivations and consume resources",
-		enabled: true,
-		showCommand: false,
-	},
-	{
-		pattern: /\bnixos-rebuild\s+(switch|boot|test)\b/,
-		description: "direct nixos-rebuild",
-		action: "block",
-		reason: "Direct nixos-rebuild bypasses make targets and safety checks",
-		enabled: true,
-		suggestion: "Use 'make switch', 'make boot', or 'make host/<hostname>/switch' instead",
-	},
-	{
-		pattern: /\bhome-manager\s+switch\b/,
-		description: "direct home-manager switch",
-		action: "block",
-		reason: "Direct home-manager switch bypasses make targets",
-		enabled: true,
-		suggestion: "Use 'make switch' or appropriate make target instead",
-	},
-	{
-		pattern: /\bnix\s+store\s+(delete|gc|optimise)\b/,
-		description: "nix store modification",
-		action: "confirm",
-		reason: "Store operations can delete or modify nix store contents",
-		enabled: true,
-		showCommand: true,
-	},
-	{
-		pattern: /\bnix-collect-garbage\b/,
-		description: "nix garbage collection",
-		action: "confirm",
-		reason: "Garbage collection will delete unreferenced store paths",
-		enabled: true,
-		showCommand: true,
-	},
-	{
-		pattern: /\bnix-channel\s+(--add|--remove|--update)\b/,
-		description: "nix-channel modification",
-		action: "confirm",
-		reason: "Channel operations modify available package sources",
-		enabled: true,
-		showCommand: false,
-	},
-	{
-		pattern: /\bnix\s+flake\s+update\b/,
-		description: "nix flake update",
-		action: "confirm",
-		reason: "Flake updates will modify flake.lock and change input versions",
-		enabled: true,
-		showCommand: false,
-	},
-	{
-		pattern: /\bnix\s+flake\s+lock\b/,
-		description: "nix flake lock",
-		action: "confirm",
-		reason: "Flake lock will modify flake.lock",
-		enabled: true,
-		showCommand: false,
-	},
-	{
-		pattern: /\bnix\s+(copy|push)\b/,
-		description: "nix store transfer",
-		action: "confirm",
-		reason: "Store transfers can push/copy potentially large store paths",
-		enabled: true,
-		showCommand: true,
-	},
-	{
-		pattern: /\bnix\s+(develop|shell)\b/,
-		description: "nix development shell",
-		action: "confirm",
-		reason: "Entering a nix shell changes the environment context",
-		enabled: true,
-		showCommand: false,
-	},
-	{
-		pattern: /\bnix\s+run\b/,
-		description: "nix run (execute package)",
-		action: "confirm",
-		reason: "nix run executes packages directly",
-		enabled: true,
-		showCommand: true,
-	},
-	{
-		pattern: /\bnix-env\s+-[ie]/,
-		description: "nix-env package installation",
-		action: "confirm",
-		reason: "nix-env modifies user environment (prefer declarative config)",
-		enabled: true,
-		showCommand: true,
-		suggestion: "Consider using home-manager or system configuration instead",
-	},
-	{
-		pattern: /\bnix\s+profile\s+(install|remove|upgrade)\b/,
-		description: "nix profile modification",
-		action: "confirm",
-		reason: "Profile operations modify user profile (prefer declarative config)",
-		enabled: true,
-		showCommand: true,
-		suggestion: "Consider using home-manager or system configuration instead",
-	},
-];
-
-// ── Helpers ───────────────────────────────────────────────────
-
-/** Strip content inside quotes to avoid false positives on e.g. commit messages */
-function stripQuotedContent(command: string): string {
-	return command
-		.replace(/"(?:[^"\\]|\\.)*"/g, '""')
-		.replace(/'[^']*'/g, "''");
-}
-
-// ── Extension entry point ─────────────────────────────────────
-
-export default function (pi: ExtensionAPI) {
-	const CONFIG_PATH = join(homedir(), ".config", "ai", "nix-guardrails.json");
-	let rules: NixCommandRule[] = DEFAULT_RULES;
-	let configLoaded = false;
-
-	async function loadConfig(): Promise<void> {
-		if (configLoaded) return;
-		try {
-			if (existsSync(CONFIG_PATH)) {
-				const content = await readFile(CONFIG_PATH, "utf-8");
-				const config: NixGuardrailsConfig = JSON.parse(content);
-				if (config.rules && Array.isArray(config.rules)) {
-					rules = config.rules.map((r) => ({
-						pattern: new RegExp(r.pattern),
-						description: r.description,
-						action: r.action,
-						reason: r.reason,
-						enabled: r.enabled !== false,
-						showCommand: r.showCommand,
-						suggestion: r.suggestion,
-					}));
-				}
-			}
-		} catch (error) {
-			console.error(`[nix-guardrails] Error loading config: ${error}`);
-		}
-		configLoaded = true;
-	}
-
-	// ── Save default config ───────────────────────────────────
-
-	async function saveDefaultConfig(): Promise<void> {
-		try {
-			const configDir = join(homedir(), ".config", "ai");
-			const { mkdir } = await import("node:fs/promises");
-			await mkdir(configDir, { recursive: true });
-
-			const config: NixGuardrailsConfig = {
-				rules: DEFAULT_RULES.map((r) => ({
-					pattern: r.pattern.source,
-					description: r.description,
-					action: r.action,
-					reason: r.reason,
-					enabled: r.enabled,
-					showCommand: r.showCommand,
-					suggestion: r.suggestion,
-				})),
-			};
-
-			await writeFile(CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
-		} catch (error) {
-			console.error(`[nix-guardrails] Error saving config: ${error}`);
-		}
-	}
-
-	// ── Tool call hook: intercept bash commands ──────────────
-
-	pi.on("tool_call", async (event, ctx) => {
-		await loadConfig();
-
-		if (event.toolName?.toLowerCase() !== "bash") {
-			return undefined;
-		}
-
-		const command = (event.input.command as string) || "";
-		const commandForMatching = stripQuotedContent(command);
-
-		// Check each rule
-		for (const rule of rules) {
-			if (!rule.enabled) continue;
-
-			if (rule.pattern.test(commandForMatching)) {
-				const theme = ctx.ui.theme;
-
-				// Handle different actions
-				if (rule.action === "block") {
-					const message = [
-						theme.fg("error", `🚫 BLOCKED: ${rule.description}`),
-						theme.fg("dim", `   Reason: ${rule.reason}`),
-					];
-					if (rule.suggestion) {
-						message.push(theme.fg("accent", `   💡 ${rule.suggestion}`));
-					}
-					message.push(theme.fg("dim", `   Command: ${command}`));
-
-					ctx.ui.notify(message.join("\n"), "error");
-					return {
-						block: true,
-						reason: `${rule.description}: ${rule.reason}${
-							rule.suggestion ? `\n${rule.suggestion}` : ""
-						}`,
-					};
-				}
-
-				if (rule.action === "confirm") {
-					if (!ctx.hasUI) {
-						return {
-							block: true,
-							reason: `${rule.description} requires approval (no UI available)`,
-						};
-					}
-
-					const confirmMessage = [
-						rule.reason,
-						rule.suggestion ? `\n💡 ${rule.suggestion}` : "",
-					]
-						.filter(Boolean)
-						.join("");
-
-					const displayCommand = rule.showCommand !== false ? command : undefined;
-
-					const ok = await ctx.ui.confirm(`⚠️ ${rule.description}`, confirmMessage, displayCommand);
-
-					if (!ok) {
-						return {
-							block: true,
-							reason: `${rule.description} blocked by user`,
-						};
-					}
-					// User approved, continue
-					break;
-				}
-
-				// rule.action === "allow" - no-op, but prevents later rules from matching
-				if (rule.action === "allow") {
-					break;
-				}
-			}
-		}
-
-		return undefined;
-	});
-
-	// ── Commands ──────────────────────────────────────────────
-
-	pi.registerCommand("nix-rules", {
-		description: "List Nix guardrail rules. Usage: /nix-rules [filter]",
-		handler: async (args, ctx) => {
-			await loadConfig();
-			const theme = ctx.ui.theme;
-
-			const filter = (args || "").trim().toLowerCase();
-			let filteredRules = rules;
-			if (filter) {
-				filteredRules = rules.filter(
-					(r) =>
-						r.description.toLowerCase().includes(filter) ||
-						r.action.toLowerCase() === filter ||
-						r.pattern.source.toLowerCase().includes(filter)
-				);
-			}
-
-			if (filteredRules.length === 0) {
-				ctx.ui.notify(`No rules matching "${filter}"`, "info");
-				return;
-			}
-
-			const lines: string[] = [
-				theme.bold(`🔒 Nix Guardrail Rules${filter ? ` (${filter})` : ""} [${filteredRules.length}/${rules.length}]`),
-				theme.fg("dim", "─".repeat(70)),
-			];
-
-			for (const rule of filteredRules) {
-				const status = rule.enabled ? theme.fg("success", "on") : theme.fg("dim", "off");
-				const actionColor = rule.action === "block" ? "error" : rule.action === "confirm" ? "warning" : "success";
-				const action = theme.fg(actionColor, rule.action.padEnd(7));
-				const icon = rule.action === "block" ? "🚫" : rule.action === "confirm" ? "⚠️" : "✅";
-
-				lines.push(`${icon} ${action} [${status}] ${theme.bold(rule.description)}`);
-				lines.push(`  ${theme.fg("dim", `Pattern: ${rule.pattern.source}`)}`);
-				lines.push(`  ${theme.fg("dim", `Reason: ${rule.reason}`)}`);
-				if (rule.suggestion) {
-					lines.push(`  ${theme.fg("accent", `💡 ${rule.suggestion}`)}`);
-				}
-				lines.push("");
-			}
-
-			lines.push(theme.fg("dim", `Config: ${CONFIG_PATH}`));
-			lines.push(theme.fg("dim", "Use /nix-rules-save to save default config"));
-
-			const tmpFile = `/tmp/nix-rules-${Date.now()}.txt`;
-			const plainLines = lines.map((l) => l.replace(/\x1b\[[0-9;]*m/g, ""));
-			await writeFile(tmpFile, plainLines.join("\n"), "utf-8");
-
-			ctx.ui.setWidget("nix-rules", lines);
-			ctx.ui.notify(`Full output: ${tmpFile}`, "info");
-			setTimeout(() => ctx.ui.setWidget("nix-rules", undefined), 30000);
-		},
-	});
-
-	pi.registerCommand("nix-rules-reload", {
-		description: "Reload Nix guardrail rules from config file",
-		handler: async (_args, ctx) => {
-			configLoaded = false;
-			await loadConfig();
-			ctx.ui.notify(`Loaded ${rules.length} Nix guardrail rules`, "success");
-		},
-	});
-
-	pi.registerCommand("nix-rules-save", {
-		description: "Save default Nix guardrail rules to config file",
-		handler: async (_args, ctx) => {
-			await saveDefaultConfig();
-			ctx.ui.notify(`Saved default config to ${CONFIG_PATH}`, "success");
-		},
-	});
-}
dots/pi/agent/extensions/nix-guardrails/package.json
@@ -1,12 +0,0 @@
-{
-  "name": "nix-guardrails",
-  "version": "1.0.0",
-  "description": "Nix command guardrails for pi-coding-agent",
-  "type": "module",
-  "pi": {
-    "extensions": ["./index.ts"]
-  },
-  "devDependencies": {
-    "bun-types": "^1.0.0"
-  }
-}
dots/pi/agent/extensions/nix-guardrails/README.md
@@ -1,181 +0,0 @@
-# Nix Guardrails Extension
-
-Pi extension that requires approval for potentially dangerous or impactful Nix commands.
-
-## Features
-
-### Protected Commands
-
-The extension intercepts and requires approval for:
-
-- **`nix eval`** - Can execute arbitrary Nix code with side effects
-- **`nix-build`** - Builds derivations and consumes resources
-- **`nixos-rebuild`** - Direct system changes (BLOCKED - use make targets instead)
-- **`home-manager switch`** - Direct home changes (BLOCKED - use make targets instead)
-- **`nix store` operations** - Store modifications, deletion, optimization
-- **`nix-collect-garbage`** - Deletes unreferenced store paths
-- **`nix-channel`** - Modifies package sources
-- **`nix flake update/lock`** - Modifies flake.lock file
-- **`nix copy/push`** - Store transfers (potentially large)
-- **`nix develop/shell`** - Changes environment context
-- **`nix run`** - Executes packages directly
-- **`nix-env`** - User environment modifications (suggests declarative alternatives)
-- **`nix profile`** - Profile modifications (suggests declarative alternatives)
-
-### Action Types
-
-- **confirm** - Shows a confirmation dialog before executing
-- **block** - Prevents execution entirely with suggested alternative
-- **allow** - Explicitly allows (for overriding other rules)
-
-## Configuration
-
-The extension can be configured via `~/.config/ai/nix-guardrails.json`:
-
-```json
-{
-  "rules": [
-    {
-      "pattern": "\\bnix\\s+eval\\b",
-      "description": "nix eval (arbitrary code execution)",
-      "action": "confirm",
-      "reason": "nix eval can execute arbitrary Nix code and has side effects",
-      "enabled": true,
-      "showCommand": true
-    },
-    {
-      "pattern": "\\bnixos-rebuild\\s+(switch|boot|test)\\b",
-      "description": "direct nixos-rebuild",
-      "action": "block",
-      "reason": "Direct nixos-rebuild bypasses make targets and safety checks",
-      "enabled": true,
-      "suggestion": "Use 'make switch', 'make boot', or 'make host/<hostname>/switch' instead"
-    }
-  ]
-}
-```
-
-### Configuration Fields
-
-- **pattern** - Regular expression to match commands
-- **description** - Human-readable description
-- **action** - `"confirm"`, `"block"`, or `"allow"`
-- **reason** - Why this rule exists
-- **enabled** - Whether the rule is active (default: `true`)
-- **showCommand** - Show full command in confirmation (default: `false` for confirm, `true` for block)
-- **suggestion** - Alternative command/approach to suggest
-
-## Commands
-
-### `/nix-rules [filter]`
-
-List all Nix guardrail rules. Optional filter by description, action, or pattern.
-
-```
-/nix-rules              # List all rules
-/nix-rules eval         # Filter by "eval"
-/nix-rules block        # Show only blocking rules
-```
-
-### `/nix-rules-reload`
-
-Reload configuration from `~/.config/ai/nix-guardrails.json`.
-
-### `/nix-rules-save`
-
-Save the default rule configuration to `~/.config/ai/nix-guardrails.json`. Useful for creating an initial config file to customize.
-
-## Usage Examples
-
-### Example 1: nix eval (confirmation)
-
-When Pi tries to run `nix eval .#nixosConfigurations.rhea.config.networking.hostName`:
-
-```
-⚠️ nix eval (arbitrary code execution)
-
-nix eval can execute arbitrary Nix code and has side effects
-
-Command: nix eval .#nixosConfigurations.rhea.config.networking.hostName
-
-[Approve] [Reject]
-```
-
-### Example 2: nixos-rebuild (blocked)
-
-When Pi tries to run `nixos-rebuild switch`:
-
-```
-🚫 BLOCKED: direct nixos-rebuild
-   Reason: Direct nixos-rebuild bypasses make targets and safety checks
-   💡 Use 'make switch', 'make boot', or 'make host/<hostname>/switch' instead
-   Command: nixos-rebuild switch
-```
-
-The command is blocked entirely and not executed.
-
-### Example 3: nix flake update (confirmation)
-
-When Pi tries to run `nix flake update`:
-
-```
-⚠️ nix flake update
-
-Flake updates will modify flake.lock and change input versions
-
-[Approve] [Reject]
-```
-
-## Customization
-
-To customize rules:
-
-1. Run `/nix-rules-save` to create the default config
-2. Edit `~/.config/ai/nix-guardrails.json`
-3. Modify, add, or remove rules
-4. Run `/nix-rules-reload` to apply changes
-
-Example customizations:
-
-```json
-{
-  "rules": [
-    // Disable nix eval confirmation
-    {
-      "pattern": "\\bnix\\s+eval\\b",
-      "description": "nix eval",
-      "action": "allow",
-      "reason": "Allow nix eval without confirmation",
-      "enabled": true
-    },
-    // Add custom rule for nix build
-    {
-      "pattern": "\\bnix\\s+build.*--impure",
-      "description": "impure nix build",
-      "action": "block",
-      "reason": "Impure builds should not be used in this project",
-      "enabled": true,
-      "suggestion": "Remove --impure flag"
-    }
-  ]
-}
-```
-
-## Integration with Homelab Repository
-
-This extension is particularly useful for the homelab repository where:
-
-- `nixos-rebuild` and `home-manager` should always go through make targets for safety
-- `nix eval` is occasionally needed for debugging but should be explicit
-- Flake updates should be intentional and approved
-- Store operations can have significant impact
-
-## Development
-
-The extension uses Pi's `tool_call` event hook to intercept bash commands before execution. It checks each command against configured rules and either:
-
-1. Blocks execution (returns `{ block: true, reason: "..." }`)
-2. Shows confirmation dialog (uses `ctx.ui.confirm()`)
-3. Allows execution (returns `undefined`)
-
-Pattern matching is done with JavaScript RegExp, and rules are processed in order (first match wins).