diff --git a/lib/ai/claude_cli.js b/lib/ai/claude_cli.js index 569a30d..e8e222e 100644 --- a/lib/ai/claude_cli.js +++ b/lib/ai/claude_cli.js @@ -58,6 +58,7 @@ import { createInterface } from 'readline'; * @param {string[]} [opts.allowedTools] Tool names to allow (--allowedTools multi-value) * @param {function} [opts.onEvent] Called for each normalized event * @param {string} [opts.claudeExe] Path or name of claude binary (default: CLAUDE_EXE env or 'claude') + * @param {string[]} [opts.tools] Exclusive available-tools allowlist (--tools); removes built-ins * @param {string} [opts.home] If set, overrides HOME in child env (for service-user creds) * @param {string} [opts.cwd] Working directory for the child process * @param {number} [opts.timeoutMs] Milliseconds before SIGTERM (default: 600000) @@ -71,6 +72,7 @@ export async function runClaudeTurn(opts) { userText, mcpConfigPath, allowedTools = [], + tools = [], onEvent, claudeExe = process.env.CLAUDE_EXE || 'claude', home = process.env.VOID_CLAUDE_HOME, @@ -94,6 +96,12 @@ export async function runClaudeTurn(opts) { args.push('--mcp-config', mcpConfigPath, '--strict-mcp-config'); } + if (tools.length > 0) { + // --tools is the EXCLUSIVE availability allowlist: restricts the session to + // exactly these tools, removing claude's built-ins (Bash/Read/Write/Grep/…). + args.push('--tools', ...tools); + } + if (allowedTools.length > 0) { // --allowedTools accepts space-separated list as multiple values under one flag args.push('--allowedTools', ...allowedTools); diff --git a/lib/api/routes/companion.js b/lib/api/routes/companion.js index 50fdf40..47aacb0 100644 --- a/lib/api/routes/companion.js +++ b/lib/api/routes/companion.js @@ -75,7 +75,9 @@ spacesScopedRouter.post('/turn', const mcpConfig = { mcpServers: { void: { - command: 'node', + // Absolute node path: claude resolves `command` against the MCP child's + // env (which has no PATH), so a bare 'node' fails to spawn ("status:failed"). + command: process.execPath, args: [COMPANION_STDIO_PATH], env: { VOID_SPACE_ID: req.params.space_id, @@ -92,6 +94,13 @@ spacesScopedRouter.post('/turn', const claudeExe = req.app.locals.claudeExe || process.env.CLAUDE_EXE || 'claude'; const draftIds = []; + const companionTools = [ + 'mcp__void__search', + 'mcp__void__read', + 'mcp__void__context', + 'mcp__void__propose_change' + ]; + let result; try { result = await runClaudeTurn({ @@ -99,12 +108,10 @@ spacesScopedRouter.post('/turn', systemPrompt: SYSTEM, userText: text, mcpConfigPath, - allowedTools: [ - 'mcp__void__search', - 'mcp__void__read', - 'mcp__void__context', - 'mcp__void__propose_change' - ], + // `tools` restricts the session to ONLY our tools (no built-in Bash/Read/…); + // `allowedTools` auto-approves them in non-interactive (--print) mode. + tools: companionTools, + allowedTools: companionTools, claudeExe, home: process.env.VOID_CLAUDE_HOME || undefined, onEvent: (e) => {