Commit 1be16d327ce0

Vincent Demeester <vincent@sbr.pm>
2026-02-08 22:39:49
revert: removed sandbox extension, restored uv.ts
Sandbox extension was too problematic: - Only sandboxes bash commands, not read/write/edit tools - bwrap configuration issues with ~/.claude/debug - Not providing the expected level of protection Restored original uv.ts extension for Python tool interception.
1 parent 83dd349
Changed files (6)
.pi/sandbox.json
@@ -1,4 +0,0 @@
-{
-  "enabled": false,
-  "comment": "Sandbox disabled for homelab - use --no-sandbox flag or remove this file to enable"
-}
dots/pi/agent/extensions/sandbox/index.ts
@@ -1,344 +0,0 @@
-/**
- * Sandbox Extension - OS-level sandboxing for bash commands
- *
- * Uses @anthropic-ai/sandbox-runtime to enforce filesystem and network
- * restrictions on bash commands at the OS level (sandbox-exec on macOS,
- * bubblewrap on Linux).
- *
- * Config files (merged, project takes precedence):
- * - ~/.pi/agent/sandbox.json (global)
- * - <cwd>/.pi/sandbox.json (project-local)
- *
- * Example .pi/sandbox.json:
- * ```json
- * {
- *   "enabled": true,
- *   "network": {
- *     "allowedDomains": ["github.com", "*.github.com"],
- *     "deniedDomains": []
- *   },
- *   "filesystem": {
- *     "denyRead": ["~/.ssh", "~/.aws"],
- *     "allowWrite": [".", "/tmp"],
- *     "denyWrite": [".env"]
- *   }
- * }
- * ```
- *
- * Usage:
- * - `pi` - sandbox enabled with default/config settings
- * - `pi --no-sandbox` - disable sandboxing
- * - `/sandbox` - show current sandbox configuration
- *
- * Linux requires: bubblewrap, socat, ripgrep
- */
-
-import { spawn } from "node:child_process";
-import { existsSync, readFileSync } from "node:fs";
-import { homedir } from "node:os";
-import { join, dirname } from "node:path";
-import { fileURLToPath } from "node:url";
-import { SandboxManager, type SandboxRuntimeConfig } from "@anthropic-ai/sandbox-runtime";
-import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
-import { type BashOperations, createBashTool } from "@mariozechner/pi-coding-agent";
-
-const __dirname = dirname(fileURLToPath(import.meta.url));
-const uvInterceptPath = join(__dirname, "..", "intercepted-commands");
-
-interface SandboxConfig extends SandboxRuntimeConfig {
-	enabled?: boolean;
-}
-
-const DEFAULT_CONFIG: SandboxConfig = {
-	enabled: true,
-	network: {
-		allowedDomains: [
-			"npmjs.org",
-			"*.npmjs.org",
-			"registry.npmjs.org",
-			"registry.yarnpkg.com",
-			"pypi.org",
-			"*.pypi.org",
-			"github.com",
-			"*.github.com",
-			"api.github.com",
-			"raw.githubusercontent.com",
-		],
-		deniedDomains: [],
-	},
-	filesystem: {
-		denyRead: ["~/.ssh", "~/.aws", "~/.gnupg"],
-		allowWrite: [".", "/tmp"],
-		denyWrite: [".env", ".env.*", "*.pem", "*.key"],
-	},
-};
-
-function loadConfig(cwd: string): SandboxConfig {
-	const projectConfigPath = join(cwd, ".pi", "sandbox.json");
-	const globalConfigPath = join(homedir(), ".pi", "agent", "sandbox.json");
-
-	let globalConfig: Partial<SandboxConfig> = {};
-	let projectConfig: Partial<SandboxConfig> = {};
-
-	if (existsSync(globalConfigPath)) {
-		try {
-			globalConfig = JSON.parse(readFileSync(globalConfigPath, "utf-8"));
-		} catch (e) {
-			console.error(`Warning: Could not parse ${globalConfigPath}: ${e}`);
-		}
-	}
-
-	if (existsSync(projectConfigPath)) {
-		try {
-			projectConfig = JSON.parse(readFileSync(projectConfigPath, "utf-8"));
-		} catch (e) {
-			console.error(`Warning: Could not parse ${projectConfigPath}: ${e}`);
-		}
-	}
-
-	return deepMerge(deepMerge(DEFAULT_CONFIG, globalConfig), projectConfig);
-}
-
-function deepMerge(base: SandboxConfig, overrides: Partial<SandboxConfig>): SandboxConfig {
-	const result: SandboxConfig = { ...base };
-
-	if (overrides.enabled !== undefined) result.enabled = overrides.enabled;
-	if (overrides.network) {
-		result.network = { ...base.network, ...overrides.network };
-	}
-	if (overrides.filesystem) {
-		result.filesystem = { ...base.filesystem, ...overrides.filesystem };
-	}
-
-	const extOverrides = overrides as {
-		ignoreViolations?: Record<string, string[]>;
-		enableWeakerNestedSandbox?: boolean;
-	};
-	const extResult = result as { ignoreViolations?: Record<string, string[]>; enableWeakerNestedSandbox?: boolean };
-
-	if (extOverrides.ignoreViolations) {
-		extResult.ignoreViolations = extOverrides.ignoreViolations;
-	}
-	if (extOverrides.enableWeakerNestedSandbox !== undefined) {
-		extResult.enableWeakerNestedSandbox = extOverrides.enableWeakerNestedSandbox;
-	}
-
-	return result;
-}
-
-function createSandboxedBashOps(): BashOperations {
-	return {
-		async exec(command, cwd, { onData, signal, timeout }) {
-			if (!existsSync(cwd)) {
-				throw new Error(`Working directory does not exist: ${cwd}`);
-			}
-
-			const wrappedCommand = await SandboxManager.wrapWithSandbox(command);
-
-			return new Promise((resolve, reject) => {
-				const child = spawn("bash", ["-c", wrappedCommand], {
-					cwd,
-					detached: true,
-					stdio: ["ignore", "pipe", "pipe"],
-				});
-
-				let timedOut = false;
-				let timeoutHandle: NodeJS.Timeout | undefined;
-
-				if (timeout !== undefined && timeout > 0) {
-					timeoutHandle = setTimeout(() => {
-						timedOut = true;
-						if (child.pid) {
-							try {
-								process.kill(-child.pid, "SIGKILL");
-							} catch {
-								child.kill("SIGKILL");
-							}
-						}
-					}, timeout * 1000);
-				}
-
-				child.stdout?.on("data", onData);
-				child.stderr?.on("data", onData);
-
-				child.on("error", (err) => {
-					if (timeoutHandle) clearTimeout(timeoutHandle);
-					reject(err);
-				});
-
-				const onAbort = () => {
-					if (child.pid) {
-						try {
-							process.kill(-child.pid, "SIGKILL");
-						} catch {
-							child.kill("SIGKILL");
-						}
-					}
-				};
-
-				signal?.addEventListener("abort", onAbort, { once: true });
-
-				child.on("close", (code) => {
-					if (timeoutHandle) clearTimeout(timeoutHandle);
-					signal?.removeEventListener("abort", onAbort);
-
-					if (signal?.aborted) {
-						reject(new Error("aborted"));
-					} else if (timedOut) {
-						reject(new Error(`timeout:${timeout}`));
-					} else {
-						resolve({ exitCode: code });
-					}
-				});
-			});
-		},
-	};
-}
-
-export default function (pi: ExtensionAPI) {
-	pi.registerFlag("no-sandbox", {
-		description: "Disable OS-level sandboxing for bash commands",
-		type: "boolean",
-		default: false,
-	});
-
-	const localCwd = process.cwd();
-	let sandboxEnabled = false;
-	let sandboxInitialized = false;
-	let uvInterceptEnabled = false;
-
-	// Check if uv intercept directory exists
-	if (existsSync(uvInterceptPath)) {
-		uvInterceptEnabled = true;
-	}
-
-	// Create bash tool that dynamically uses sandboxed operations based on current state
-	const bashTool = createBashTool(localCwd, {
-		spawnHook: ({ command, cwd, env }) => {
-			let modifiedCommand = command;
-			
-			// Add UV intercept to PATH if enabled
-			if (uvInterceptEnabled) {
-				modifiedCommand = `export PATH="${uvInterceptPath}:$PATH"\n${modifiedCommand}`;
-			}
-			
-			return { command: modifiedCommand, cwd, env };
-		},
-	});
-
-	// Override the bash tool to dynamically use sandbox operations
-	const originalExecute = bashTool.execute;
-	bashTool.execute = async function (toolCallId, params, signal, onUpdate, ctx) {
-		// If sandbox is enabled, use sandboxed operations
-		if (sandboxEnabled && sandboxInitialized) {
-			const sandboxedTool = createBashTool(localCwd, {
-				spawnHook: bashTool.spawnHook,
-				operations: createSandboxedBashOps(),
-			});
-			return sandboxedTool.execute(toolCallId, params, signal, onUpdate, ctx);
-		}
-		// Otherwise use original (non-sandboxed)
-		return originalExecute.call(bashTool, toolCallId, params, signal, onUpdate, ctx);
-	};
-
-	pi.registerTool(bashTool);
-
-	// For user bash commands (! and !!), provide sandboxed operations
-	pi.on("user_bash", () => {
-		if (!sandboxEnabled || !sandboxInitialized) return;
-		return { operations: createSandboxedBashOps() };
-	});
-
-	pi.on("session_start", async (_event, ctx) => {
-		const noSandbox = pi.getFlag("no-sandbox") as boolean;
-
-		if (noSandbox) {
-			sandboxEnabled = false;
-			ctx.ui.notify("Sandbox disabled via --no-sandbox", "warning");
-			return;
-		}
-
-		const config = loadConfig(ctx.cwd);
-
-		if (!config.enabled) {
-			sandboxEnabled = false;
-			ctx.ui.notify("Sandbox disabled via config", "info");
-			return;
-		}
-
-		const platform = process.platform;
-		if (platform !== "darwin" && platform !== "linux") {
-			sandboxEnabled = false;
-			ctx.ui.notify(`Sandbox not supported on ${platform}`, "warning");
-			return;
-		}
-
-		try {
-			const configExt = config as unknown as {
-				ignoreViolations?: Record<string, string[]>;
-				enableWeakerNestedSandbox?: boolean;
-			};
-
-			await SandboxManager.initialize({
-				network: config.network,
-				filesystem: config.filesystem,
-				ignoreViolations: configExt.ignoreViolations,
-				enableWeakerNestedSandbox: configExt.enableWeakerNestedSandbox,
-			});
-
-			sandboxEnabled = true;
-			sandboxInitialized = true;
-
-			const networkCount = config.network?.allowedDomains?.length ?? 0;
-			const writeCount = config.filesystem?.allowWrite?.length ?? 0;
-			ctx.ui.setStatus(
-				"sandbox",
-				ctx.ui.theme.fg("accent", `๐Ÿ”’ Sandbox: ${networkCount} domains, ${writeCount} write paths`),
-			);
-			
-			const messages = ["Sandbox initialized"];
-			if (uvInterceptEnabled) {
-				messages.push("UV interceptor active (pip/poetry/pipenv โ†’ uv)");
-			}
-			ctx.ui.notify(messages.join("\n"), "info");
-		} catch (err) {
-			sandboxEnabled = false;
-			ctx.ui.notify(`Sandbox initialization failed: ${err instanceof Error ? err.message : err}`, "error");
-		}
-	});
-
-	pi.on("session_shutdown", async () => {
-		if (sandboxInitialized) {
-			try {
-				await SandboxManager.reset();
-			} catch {
-				// Ignore cleanup errors
-			}
-		}
-	});
-
-	pi.registerCommand("sandbox", {
-		description: "Show sandbox configuration",
-		handler: async (_args, ctx) => {
-			if (!sandboxEnabled) {
-				ctx.ui.notify("Sandbox is disabled", "info");
-				return;
-			}
-
-			const config = loadConfig(ctx.cwd);
-			const lines = [
-				"Sandbox Configuration:",
-				"",
-				"Network:",
-				`  Allowed: ${config.network?.allowedDomains?.join(", ") || "(none)"}`,
-				`  Denied: ${config.network?.deniedDomains?.join(", ") || "(none)"}`,
-				"",
-				"Filesystem:",
-				`  Deny Read: ${config.filesystem?.denyRead?.join(", ") || "(none)"}`,
-				`  Allow Write: ${config.filesystem?.allowWrite?.join(", ") || "(none)"}`,
-				`  Deny Write: ${config.filesystem?.denyWrite?.join(", ") || "(none)"}`,
-			];
-			ctx.ui.notify(lines.join("\n"), "info");
-		},
-	});
-}
dots/pi/agent/extensions/sandbox/package-lock.json
@@ -1,92 +0,0 @@
-{
-  "name": "sandbox",
-  "version": "1.0.0",
-  "lockfileVersion": 3,
-  "requires": true,
-  "packages": {
-    "": {
-      "name": "sandbox",
-      "version": "1.0.0",
-      "dependencies": {
-        "@anthropic-ai/sandbox-runtime": "^0.0.34"
-      }
-    },
-    "node_modules/@anthropic-ai/sandbox-runtime": {
-      "version": "0.0.34",
-      "resolved": "https://registry.npmjs.org/@anthropic-ai/sandbox-runtime/-/sandbox-runtime-0.0.34.tgz",
-      "integrity": "sha512-kdzOfa1X7gB1bmkLsdMQYAE+YpvE6LO7ZjYu4HhCvxyUQl0cvU+B806QW7yp5c/m6swZuiboogtHKAfXRRTRYA==",
-      "license": "Apache-2.0",
-      "dependencies": {
-        "@pondwader/socks5-server": "^1.0.10",
-        "@types/lodash-es": "^4.17.12",
-        "commander": "^12.1.0",
-        "lodash-es": "^4.17.23",
-        "shell-quote": "^1.8.3",
-        "zod": "^3.24.1"
-      },
-      "bin": {
-        "srt": "dist/cli.js"
-      },
-      "engines": {
-        "node": ">=18.0.0"
-      }
-    },
-    "node_modules/@pondwader/socks5-server": {
-      "version": "1.0.10",
-      "resolved": "https://registry.npmjs.org/@pondwader/socks5-server/-/socks5-server-1.0.10.tgz",
-      "integrity": "sha512-bQY06wzzR8D2+vVCUoBsr5QS2U6UgPUQRmErNwtsuI6vLcyRKkafjkr3KxbtGFf9aBBIV2mcvlsKD1UYaIV+sg==",
-      "license": "MIT"
-    },
-    "node_modules/@types/lodash": {
-      "version": "4.17.23",
-      "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.23.tgz",
-      "integrity": "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==",
-      "license": "MIT"
-    },
-    "node_modules/@types/lodash-es": {
-      "version": "4.17.12",
-      "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
-      "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
-      "license": "MIT",
-      "dependencies": {
-        "@types/lodash": "*"
-      }
-    },
-    "node_modules/commander": {
-      "version": "12.1.0",
-      "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
-      "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
-      "license": "MIT",
-      "engines": {
-        "node": ">=18"
-      }
-    },
-    "node_modules/lodash-es": {
-      "version": "4.17.23",
-      "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz",
-      "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==",
-      "license": "MIT"
-    },
-    "node_modules/shell-quote": {
-      "version": "1.8.3",
-      "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
-      "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/zod": {
-      "version": "3.25.76",
-      "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
-      "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
-      "license": "MIT",
-      "funding": {
-        "url": "https://github.com/sponsors/colinhacks"
-      }
-    }
-  }
-}
dots/pi/agent/extensions/sandbox/package.json
@@ -1,8 +0,0 @@
-{
-  "name": "sandbox",
-  "version": "1.0.0",
-  "type": "module",
-  "dependencies": {
-    "@anthropic-ai/sandbox-runtime": "^0.0.34"
-  }
-}
dots/pi/agent/extensions/sandbox/README.md
@@ -1,224 +0,0 @@
-# Sandbox Extension
-
-OS-level sandboxing for bash commands using [@anthropic-ai/sandbox-runtime](https://www.npmjs.com/package/@anthropic-ai/sandbox-runtime).
-
-## Features
-
-- **Network restrictions**: Limit which domains bash commands can access
-- **Filesystem restrictions**: Control what paths can be read/written
-- **OS-level enforcement**: Uses `sandbox-exec` (macOS) or `bubblewrap` (Linux)
-- **Configuration**: Global and project-local configs with merging
-
-## Requirements
-
-### Linux (NixOS)
-
-The following packages are required on Linux:
-
-```nix
-# In your NixOS configuration or home-manager:
-home.packages = with pkgs; [
-  bubblewrap
-  socat
-  ripgrep
-];
-```
-
-Or install manually:
-```bash
-nix-shell -p bubblewrap socat ripgrep
-```
-
-**Additional setup:**
-
-The sandbox runtime needs a debug directory:
-```bash
-mkdir -p ~/.claude/debug
-```
-
-This directory is used by `@anthropic-ai/sandbox-runtime` for logging and temporary files.
-
-### macOS
-
-Uses built-in `sandbox-exec`, no additional packages required.
-
-## Configuration
-
-Configuration files are merged with project-local taking precedence:
-
-1. `~/.pi/agent/sandbox.json` (global defaults)
-2. `<project>/.pi/sandbox.json` (project overrides)
-
-### Example Configuration
-
-```json
-{
-  "enabled": true,
-  "network": {
-    "allowedDomains": [
-      "github.com",
-      "*.github.com",
-      "npmjs.org",
-      "*.npmjs.org"
-    ],
-    "deniedDomains": []
-  },
-  "filesystem": {
-    "denyRead": ["~/.ssh", "~/.aws", "~/.gnupg"],
-    "allowWrite": [".", "/tmp", "~/.claude/debug"],
-    "denyWrite": [".env", ".env.*", "*.pem", "*.key"]
-  }
-}
-```
-
-### Configuration Options
-
-- `enabled` (boolean): Enable/disable sandbox globally
-- `network.allowedDomains` (string[]): Domains that bash commands can access (supports wildcards)
-- `network.deniedDomains` (string[]): Domains to explicitly block
-- `filesystem.denyRead` (string[]): Paths that cannot be read
-- `filesystem.allowWrite` (string[]): Paths that can be written to
-- `filesystem.denyWrite` (string[]): Paths that cannot be written (even in allowed write dirs)
-
-## Usage
-
-### Enable Sandbox (Default)
-
-```bash
-# Sandbox enabled with config settings
-pi
-
-# Check sandbox status
-/sandbox
-```
-
-### Disable Sandbox
-
-```bash
-# Disable via flag
-pi --no-sandbox
-
-# Or disable in project config
-echo '{"enabled": false}' > .pi/sandbox.json
-```
-
-### Commands
-
-- `/sandbox` - Show current sandbox configuration
-
-## How It Works
-
-The sandbox extension:
-
-1. Intercepts all `bash` tool calls
-2. Wraps commands with OS-level sandboxing
-3. Enforces network and filesystem restrictions
-4. Shows status in the footer
-
-**Important Limitations:**
-
-โš ๏ธ **Only `bash` commands are sandboxed.** The `read`, `write`, and `edit` tools bypass the sandbox entirely because they access files directly without going through bash.
-
-- `bash: cat ~/.ssh/config` โ†’ โœ… Sandboxed (blocked)
-- `read ~/.ssh/config` โ†’ โŒ Not sandboxed (succeeds)
-
-This means the LLM can still read/write files using the built-in tools. The sandbox primarily protects against:
-- Network access from bash commands
-- Bash scripts accessing sensitive files
-- Accidental destructive bash operations
-
-For full file access control, you would need to override the `read`, `write`, and `edit` tools or use `tool_call` event interception.
-
-### Example
-
-```bash
-# This will work (allowed domain)
-curl https://api.github.com/users/octocat
-
-# This will be blocked (not in allowedDomains)
-curl https://malicious-site.com
-
-# This will work (allowed write path)
-echo "test" > /tmp/file.txt
-
-# This will be blocked (protected path)
-cat ~/.ssh/id_rsa
-```
-
-## Project-Local Overrides
-
-For projects that need to bypass sandboxing (like homelab infrastructure), create `.pi/sandbox.json`:
-
-```json
-{
-  "enabled": false,
-  "comment": "Sandbox disabled for this project"
-}
-```
-
-Or use the `--no-sandbox` flag when working in that project.
-
-## Security Notes
-
-โš ๏ธ **Important:**
-
-- Sandboxing is **not a complete security solution**
-- It provides defense-in-depth against accidental mistakes
-- Malicious code can potentially bypass sandbox restrictions
-- Always review code before execution
-- Use for reducing attack surface, not as primary security
-
-### What It Protects Against
-
-โœ… Accidental exposure of SSH keys or credentials  
-โœ… Unintended network requests to unknown domains  
-โœ… Accidental writes to sensitive files  
-โœ… Reading protected configuration files  
-
-### What It Doesn't Protect Against
-
-โŒ Malicious code specifically designed to bypass sandbox  
-โŒ Social engineering or prompt injection  
-โŒ Vulnerabilities in the sandbox runtime itself  
-โŒ Actions taken before sandbox initialization  
-
-## Troubleshooting
-
-### "Sandbox initialization failed"
-
-On Linux, ensure `bubblewrap`, `socat`, and `ripgrep` are installed:
-
-```bash
-# NixOS
-nix-shell -p bubblewrap socat ripgrep
-
-# Verify installation
-which bwrap socat rg
-```
-
-### Commands Failing Unexpectedly
-
-Check if the command needs network or filesystem access that's blocked:
-
-```bash
-# Show current config
-/sandbox
-
-# Temporarily disable
-pi --no-sandbox
-```
-
-Add required domains/paths to your config file.
-
-### Performance Impact
-
-The sandbox has minimal performance overhead:
-- Network: DNS resolution and connection setup only
-- Filesystem: Additional syscall checks
-- Typically <100ms overhead per command
-
-## See Also
-
-- [@anthropic-ai/sandbox-runtime](https://www.npmjs.com/package/@anthropic-ai/sandbox-runtime)
-- [Pi Extensions Documentation](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/docs/extensions.md)
-- [bubblewrap](https://github.com/containers/bubblewrap)
dots/pi/agent/extensions/uv.ts.disabled โ†’ dots/pi/agent/extensions/uv.ts
@@ -26,17 +26,14 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
 const interceptedCommandsPath = join(__dirname, "intercepted-commands");
 
 export default function (pi: ExtensionAPI) {
+  const cwd = process.cwd();
+  const bashTool = createBashTool(cwd, {
+    commandPrefix: `export PATH="${interceptedCommandsPath}:$PATH"`,
+  });
+
   pi.on("session_start", (_event, ctx) => {
     ctx.ui.notify("UV interceptor active (pip/poetry/pipenv/virtualenv/conda โ†’ uv)", "info");
   });
 
-  // Don't override bash tool - instead, use tool_call event to intercept and add PATH
-  // This allows other extensions (like sandbox) to also modify bash behavior
-  pi.on("tool_call", async (event, ctx) => {
-    if (event.toolName !== "bash") return;
-    
-    // Prepend PATH modification to the command
-    const originalCommand = event.input.command;
-    event.input.command = `export PATH="${interceptedCommandsPath}:$PATH"\n${originalCommand}`;
-  });
+  pi.registerTool(bashTool);
 }