From d0f36c0f78894a98b16db96d05ddbd1849a3fdfd Mon Sep 17 00:00:00 2001 From: Roo Code Date: Mon, 14 Jul 2025 08:36:42 +0000 Subject: [PATCH] fix: replace CLAUDE_CODE_SYSTEM_PROMPT env var with temporary file approach - Remove undocumented CLAUDE_CODE_SYSTEM_PROMPT environment variable usage - Implement temporary file solution for long system prompts (>7000 chars) - Use --system-prompt @filepath pattern for file-based input - Add proper cleanup for temporary files using vscode.workspace.fs - Fix parameter naming conflict between path module and claudeCodePath parameter - Update tests to verify temporary file usage and proper cleanup - Maintain backward compatibility for short system prompts using command line args Resolves Windows ENAMETOOLONG error without relying on undocumented features. --- .../claude-code/__tests__/run.spec.ts | 77 ++++++++++++++----- src/integrations/claude-code/run.ts | 68 ++++++++++++---- 2 files changed, 111 insertions(+), 34 deletions(-) diff --git a/src/integrations/claude-code/__tests__/run.spec.ts b/src/integrations/claude-code/__tests__/run.spec.ts index dd826e6478..a538e45214 100644 --- a/src/integrations/claude-code/__tests__/run.spec.ts +++ b/src/integrations/claude-code/__tests__/run.spec.ts @@ -1,4 +1,4 @@ -import { describe, test, expect, vi, beforeEach } from "vitest" +import { describe, test, expect, vi, beforeEach, afterEach } from "vitest" // Mock vscode workspace vi.mock("vscode", () => ({ @@ -10,6 +10,13 @@ vi.mock("vscode", () => ({ }, }, ], + fs: { + writeFile: vi.fn().mockResolvedValue(undefined), + delete: vi.fn().mockResolvedValue(undefined), + }, + }, + Uri: { + file: vi.fn((path: string) => ({ fsPath: path })), }, })) @@ -86,6 +93,15 @@ vi.mock("readline", () => ({ }, })) +// Mock path and os modules +vi.mock("path", () => ({ + join: vi.fn((...args: string[]) => args.join("/")), +})) + +vi.mock("os", () => ({ + tmpdir: vi.fn(() => "/tmp"), +})) + describe("runClaudeCode", () => { beforeEach(() => { vi.clearAllMocks() @@ -313,7 +329,7 @@ describe("runClaudeCode", () => { expect(execaOptions.env?.CLAUDE_CODE_SYSTEM_PROMPT).toBeUndefined() }) - test("should use environment variable for long system prompts to avoid Windows ENAMETOOLONG error", async () => { + test("should use temporary file for long system prompts to avoid Windows ENAMETOOLONG error", async () => { const { runClaudeCode } = await import("../run") // Create a system prompt longer than MAX_COMMAND_LINE_LENGTH (7000 chars) const longSystemPrompt = "You are a helpful assistant. " + "A".repeat(7000) @@ -330,13 +346,23 @@ describe("runClaudeCode", () => { // Clean up the generator await generator.return(undefined) - // Verify execa was called without --system-prompt in command line arguments + // Verify execa was called with --system-prompt @filepath pattern const [, args, execaOptions] = mockExeca.mock.calls[0] - expect(args).not.toContain("--system-prompt") + expect(args).toContain("--system-prompt") + + // Find the system prompt argument + const systemPromptIndex = args.indexOf("--system-prompt") + expect(systemPromptIndex).toBeGreaterThan(-1) + const systemPromptArg = args[systemPromptIndex + 1] + + // Verify it uses the @filepath pattern for temp files + expect(systemPromptArg).toMatch(/^@.*claude-system-prompt-.*\.txt$/) + + // Verify the long system prompt is not directly in the arguments expect(args).not.toContain(longSystemPrompt) - // Verify environment variable was set with the long system prompt - expect(execaOptions.env?.CLAUDE_CODE_SYSTEM_PROMPT).toBe(longSystemPrompt) + // Verify no environment variable was set for system prompt + expect(execaOptions.env?.CLAUDE_CODE_SYSTEM_PROMPT).toBeUndefined() }) test("should handle exactly MAX_COMMAND_LINE_LENGTH system prompt using command line", async () => { @@ -361,11 +387,13 @@ describe("runClaudeCode", () => { expect(args).toContain("--system-prompt") expect(args).toContain(exactLengthPrompt) - // Verify no environment variable was set - expect(execaOptions.env?.CLAUDE_CODE_SYSTEM_PROMPT).toBeUndefined() + // Verify no temporary file was used (no @ prefix) + const systemPromptIndex = args.indexOf("--system-prompt") + const systemPromptArg = args[systemPromptIndex + 1] + expect(systemPromptArg).not.toMatch(/^@/) }) - test("should handle system prompt one character over threshold using environment variable", async () => { + test("should handle system prompt one character over threshold using temporary file", async () => { const { runClaudeCode } = await import("../run") // Create a system prompt one character over the threshold (7001 chars) const overThresholdPrompt = "A".repeat(7001) @@ -382,16 +410,25 @@ describe("runClaudeCode", () => { // Clean up the generator await generator.return(undefined) - // Verify execa was called without --system-prompt in command line arguments + // Verify execa was called with --system-prompt @filepath pattern const [, args, execaOptions] = mockExeca.mock.calls[0] - expect(args).not.toContain("--system-prompt") + expect(args).toContain("--system-prompt") + + // Find the system prompt argument + const systemPromptIndex = args.indexOf("--system-prompt") + const systemPromptArg = args[systemPromptIndex + 1] + + // Verify it uses the @filepath pattern for temp files + expect(systemPromptArg).toMatch(/^@.*claude-system-prompt-.*\.txt$/) + + // Verify the long system prompt is not directly in the arguments expect(args).not.toContain(overThresholdPrompt) - // Verify environment variable was set - expect(execaOptions.env?.CLAUDE_CODE_SYSTEM_PROMPT).toBe(overThresholdPrompt) + // Verify no environment variable was set + expect(execaOptions.env?.CLAUDE_CODE_SYSTEM_PROMPT).toBeUndefined() }) - test("should preserve existing environment variables when using CLAUDE_CODE_SYSTEM_PROMPT", async () => { + test("should preserve existing environment variables when using temporary files", async () => { const { runClaudeCode } = await import("../run") // Mock process.env to have some existing variables @@ -416,14 +453,16 @@ describe("runClaudeCode", () => { // Clean up the generator await generator.return(undefined) - // Verify environment variables include both existing and new ones + // Verify environment variables include existing ones but no CLAUDE_CODE_SYSTEM_PROMPT const [, , execaOptions] = mockExeca.mock.calls[0] expect(execaOptions.env).toEqual({ ...process.env, CLAUDE_CODE_MAX_OUTPUT_TOKENS: expect.any(String), // Always set by the implementation - CLAUDE_CODE_SYSTEM_PROMPT: longSystemPrompt, }) + // Verify no system prompt environment variable was set + expect(execaOptions.env?.CLAUDE_CODE_SYSTEM_PROMPT).toBeUndefined() + // Restore original environment process.env = originalEnv }) @@ -448,7 +487,9 @@ describe("runClaudeCode", () => { expect(args).toContain("--system-prompt") expect(args).toContain("") - // Verify no environment variable was set - expect(execaOptions.env?.CLAUDE_CODE_SYSTEM_PROMPT).toBeUndefined() + // Verify no temporary file was used (no @ prefix) + const systemPromptIndex = args.indexOf("--system-prompt") + const systemPromptArg = args[systemPromptIndex + 1] + expect(systemPromptArg).not.toMatch(/^@/) }) }) diff --git a/src/integrations/claude-code/run.ts b/src/integrations/claude-code/run.ts index 8bc66cb2c8..79a6468ba7 100644 --- a/src/integrations/claude-code/run.ts +++ b/src/integrations/claude-code/run.ts @@ -4,6 +4,8 @@ import { execa } from "execa" import { ClaudeCodeMessage } from "./types" import readline from "readline" import { CLAUDE_CODE_DEFAULT_MAX_OUTPUT_TOKENS } from "@roo-code/types" +import * as path from "path" +import * as os from "os" const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) @@ -21,10 +23,15 @@ type ProcessState = { exitCode: number | null } +type TempFileCleanup = { + filePath: string + cleanup: () => Promise +} + export async function* runClaudeCode( options: ClaudeCodeOptions & { maxOutputTokens?: number }, ): AsyncGenerator { - const process = runProcess(options) + const { process, tempFileCleanup } = await runProcess(options) const rl = readline.createInterface({ input: process.stdout, @@ -84,6 +91,10 @@ export async function* runClaudeCode( if (!process.killed) { process.kill() } + // Clean up temporary file if it was created + if (tempFileCleanup) { + await tempFileCleanup.cleanup() + } } } @@ -111,25 +122,55 @@ const claudeCodeTools = [ const CLAUDE_CODE_TIMEOUT = 600000 // 10 minutes // Windows has a command line length limit of ~8191 characters -// If the system prompt is too long, we'll use an environment variable instead +// If the system prompt is too long, we'll write it to a temporary file instead const MAX_COMMAND_LINE_LENGTH = 7000 // Conservative limit to account for other arguments -function runProcess({ +async function runProcess({ systemPrompt, messages, - path, + path: claudeCodePath, modelId, maxOutputTokens, -}: ClaudeCodeOptions & { maxOutputTokens?: number }) { - const claudePath = path || "claude" +}: ClaudeCodeOptions & { maxOutputTokens?: number }): Promise<{ + process: ReturnType + tempFileCleanup: TempFileCleanup | null +}> { + const claudePath = claudeCodePath || "claude" // Check if system prompt is too long for command line - const useEnvForSystemPrompt = systemPrompt.length > MAX_COMMAND_LINE_LENGTH + const useTempFileForSystemPrompt = systemPrompt.length > MAX_COMMAND_LINE_LENGTH const args = ["-p"] + let tempFileCleanup: TempFileCleanup | null = null - // Only add --system-prompt to command line if it's short enough - if (!useEnvForSystemPrompt) { + // Handle system prompt - use temp file for long prompts, command line for short ones + if (useTempFileForSystemPrompt) { + // Create temporary file for system prompt + const tempFilePath = path.join( + os.tmpdir(), + `claude-system-prompt-${Date.now()}-${Math.random().toString(36).substring(2)}.txt`, + ) + + try { + await vscode.workspace.fs.writeFile(vscode.Uri.file(tempFilePath), Buffer.from(systemPrompt, "utf8")) + args.push("--system-prompt", `@${tempFilePath}`) + + tempFileCleanup = { + filePath: tempFilePath, + cleanup: async () => { + try { + await vscode.workspace.fs.delete(vscode.Uri.file(tempFilePath)) + } catch (error) { + // Ignore cleanup errors - temp files will be cleaned up by OS eventually + console.warn(`Failed to clean up temporary system prompt file ${tempFilePath}:`, error) + } + }, + } + } catch (error) { + throw new Error(`Failed to create temporary file for system prompt: ${error}`) + } + } else { + // Use command line argument for short system prompts args.push("--system-prompt", systemPrompt) } @@ -157,11 +198,6 @@ function runProcess({ CLAUDE_CODE_DEFAULT_MAX_OUTPUT_TOKENS.toString(), } - // If system prompt is too long, pass it via environment variable - if (useEnvForSystemPrompt) { - env.CLAUDE_CODE_SYSTEM_PROMPT = systemPrompt - } - const child = execa(claudePath, args, { stdin: "pipe", stdout: "pipe", @@ -176,7 +212,7 @@ function runProcess({ // This avoids the E2BIG error on Linux and ENAMETOOLONG error on Windows when passing large data as command line arguments // Linux has a per-argument limit of ~128KiB for execve() system calls // Windows has a total command line length limit of ~8191 characters - // For system prompts, we use environment variables when they exceed the safe limit + // For system prompts, we use temporary files when they exceed the safe limit const messagesJson = JSON.stringify(messages) // Use setImmediate to ensure the process has been spawned before writing to stdin @@ -196,7 +232,7 @@ function runProcess({ } }) - return child + return { process: child, tempFileCleanup } } function parseChunk(data: string, processState: ProcessState) {