Commit ab1079cd420d
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).