Skip to content

Commit 8dc50dc

Browse files
committed
fix: resolve Windows ENAMETOOLONG error in Claude Code integration (#5631)
- Use environment variable CLAUDE_CODE_SYSTEM_PROMPT for long system prompts (>7000 chars) - Prevents Windows command line length limit (~8191 chars) from causing ENAMETOOLONG errors - Maintains backward compatibility by using command line args for short prompts - Add comprehensive tests for both short and long system prompt scenarios - Follows existing pattern used for messages parameter (stdin vs command line)
1 parent cdacdfd commit 8dc50dc

File tree

2 files changed

+198
-14
lines changed

2 files changed

+198
-14
lines changed

src/integrations/claude-code/__tests__/run.spec.ts

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,4 +287,168 @@ describe("runClaudeCode", () => {
287287
consoleErrorSpy.mockRestore()
288288
await generator.return(undefined)
289289
})
290+
291+
test("should use command line argument for short system prompts", async () => {
292+
const { runClaudeCode } = await import("../run")
293+
const shortSystemPrompt = "You are a helpful assistant"
294+
const options = {
295+
systemPrompt: shortSystemPrompt,
296+
messages: [{ role: "user" as const, content: "Hello" }],
297+
}
298+
299+
const generator = runClaudeCode(options)
300+
301+
// Consume at least one item to trigger process spawn
302+
await generator.next()
303+
304+
// Clean up the generator
305+
await generator.return(undefined)
306+
307+
// Verify execa was called with system prompt as command line argument
308+
const [, args, execaOptions] = mockExeca.mock.calls[0]
309+
expect(args).toContain("--system-prompt")
310+
expect(args).toContain(shortSystemPrompt)
311+
312+
// Verify no environment variable was set for short prompt
313+
expect(execaOptions.env?.CLAUDE_CODE_SYSTEM_PROMPT).toBeUndefined()
314+
})
315+
316+
test("should use environment variable for long system prompts to avoid Windows ENAMETOOLONG error", async () => {
317+
const { runClaudeCode } = await import("../run")
318+
// Create a system prompt longer than MAX_COMMAND_LINE_LENGTH (7000 chars)
319+
const longSystemPrompt = "You are a helpful assistant. " + "A".repeat(7000)
320+
const options = {
321+
systemPrompt: longSystemPrompt,
322+
messages: [{ role: "user" as const, content: "Hello" }],
323+
}
324+
325+
const generator = runClaudeCode(options)
326+
327+
// Consume at least one item to trigger process spawn
328+
await generator.next()
329+
330+
// Clean up the generator
331+
await generator.return(undefined)
332+
333+
// Verify execa was called without --system-prompt in command line arguments
334+
const [, args, execaOptions] = mockExeca.mock.calls[0]
335+
expect(args).not.toContain("--system-prompt")
336+
expect(args).not.toContain(longSystemPrompt)
337+
338+
// Verify environment variable was set with the long system prompt
339+
expect(execaOptions.env?.CLAUDE_CODE_SYSTEM_PROMPT).toBe(longSystemPrompt)
340+
})
341+
342+
test("should handle exactly MAX_COMMAND_LINE_LENGTH system prompt using command line", async () => {
343+
const { runClaudeCode } = await import("../run")
344+
// Create a system prompt exactly at the threshold (7000 chars)
345+
const exactLengthPrompt = "A".repeat(7000)
346+
const options = {
347+
systemPrompt: exactLengthPrompt,
348+
messages: [{ role: "user" as const, content: "Hello" }],
349+
}
350+
351+
const generator = runClaudeCode(options)
352+
353+
// Consume at least one item to trigger process spawn
354+
await generator.next()
355+
356+
// Clean up the generator
357+
await generator.return(undefined)
358+
359+
// Verify execa was called with system prompt as command line argument (at threshold)
360+
const [, args, execaOptions] = mockExeca.mock.calls[0]
361+
expect(args).toContain("--system-prompt")
362+
expect(args).toContain(exactLengthPrompt)
363+
364+
// Verify no environment variable was set
365+
expect(execaOptions.env?.CLAUDE_CODE_SYSTEM_PROMPT).toBeUndefined()
366+
})
367+
368+
test("should handle system prompt one character over threshold using environment variable", async () => {
369+
const { runClaudeCode } = await import("../run")
370+
// Create a system prompt one character over the threshold (7001 chars)
371+
const overThresholdPrompt = "A".repeat(7001)
372+
const options = {
373+
systemPrompt: overThresholdPrompt,
374+
messages: [{ role: "user" as const, content: "Hello" }],
375+
}
376+
377+
const generator = runClaudeCode(options)
378+
379+
// Consume at least one item to trigger process spawn
380+
await generator.next()
381+
382+
// Clean up the generator
383+
await generator.return(undefined)
384+
385+
// Verify execa was called without --system-prompt in command line arguments
386+
const [, args, execaOptions] = mockExeca.mock.calls[0]
387+
expect(args).not.toContain("--system-prompt")
388+
expect(args).not.toContain(overThresholdPrompt)
389+
390+
// Verify environment variable was set
391+
expect(execaOptions.env?.CLAUDE_CODE_SYSTEM_PROMPT).toBe(overThresholdPrompt)
392+
})
393+
394+
test("should preserve existing environment variables when using CLAUDE_CODE_SYSTEM_PROMPT", async () => {
395+
const { runClaudeCode } = await import("../run")
396+
397+
// Mock process.env to have some existing variables
398+
const originalEnv = process.env
399+
process.env = {
400+
...originalEnv,
401+
EXISTING_VAR: "existing_value",
402+
PATH: "/usr/bin:/bin",
403+
}
404+
405+
const longSystemPrompt = "You are a helpful assistant. " + "A".repeat(7000)
406+
const options = {
407+
systemPrompt: longSystemPrompt,
408+
messages: [{ role: "user" as const, content: "Hello" }],
409+
}
410+
411+
const generator = runClaudeCode(options)
412+
413+
// Consume at least one item to trigger process spawn
414+
await generator.next()
415+
416+
// Clean up the generator
417+
await generator.return(undefined)
418+
419+
// Verify environment variables include both existing and new ones
420+
const [, , execaOptions] = mockExeca.mock.calls[0]
421+
expect(execaOptions.env).toEqual({
422+
...process.env,
423+
CLAUDE_CODE_MAX_OUTPUT_TOKENS: expect.any(String), // Always set by the implementation
424+
CLAUDE_CODE_SYSTEM_PROMPT: longSystemPrompt,
425+
})
426+
427+
// Restore original environment
428+
process.env = originalEnv
429+
})
430+
431+
test("should work with empty system prompt", async () => {
432+
const { runClaudeCode } = await import("../run")
433+
const options = {
434+
systemPrompt: "",
435+
messages: [{ role: "user" as const, content: "Hello" }],
436+
}
437+
438+
const generator = runClaudeCode(options)
439+
440+
// Consume at least one item to trigger process spawn
441+
await generator.next()
442+
443+
// Clean up the generator
444+
await generator.return(undefined)
445+
446+
// Verify execa was called with empty system prompt as command line argument
447+
const [, args, execaOptions] = mockExeca.mock.calls[0]
448+
expect(args).toContain("--system-prompt")
449+
expect(args).toContain("")
450+
451+
// Verify no environment variable was set
452+
expect(execaOptions.env?.CLAUDE_CODE_SYSTEM_PROMPT).toBeUndefined()
453+
})
290454
})

src/integrations/claude-code/run.ts

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,10 @@ const claudeCodeTools = [
110110

111111
const CLAUDE_CODE_TIMEOUT = 600000 // 10 minutes
112112

113+
// Windows has a command line length limit of ~8191 characters
114+
// If the system prompt is too long, we'll use an environment variable instead
115+
const MAX_COMMAND_LINE_LENGTH = 7000 // Conservative limit to account for other arguments
116+
113117
function runProcess({
114118
systemPrompt,
115119
messages,
@@ -119,10 +123,17 @@ function runProcess({
119123
}: ClaudeCodeOptions & { maxOutputTokens?: number }) {
120124
const claudePath = path || "claude"
121125

122-
const args = [
123-
"-p",
124-
"--system-prompt",
125-
systemPrompt,
126+
// Check if system prompt is too long for command line
127+
const useEnvForSystemPrompt = systemPrompt.length > MAX_COMMAND_LINE_LENGTH
128+
129+
const args = ["-p"]
130+
131+
// Only add --system-prompt to command line if it's short enough
132+
if (!useEnvForSystemPrompt) {
133+
args.push("--system-prompt", systemPrompt)
134+
}
135+
136+
args.push(
126137
"--verbose",
127138
"--output-format",
128139
"stream-json",
@@ -131,32 +142,41 @@ function runProcess({
131142
// Roo Code will handle recursive calls
132143
"--max-turns",
133144
"1",
134-
]
145+
)
135146

136147
if (modelId) {
137148
args.push("--model", modelId)
138149
}
139150

151+
const env: Record<string, string> = {
152+
...process.env,
153+
// Use the configured value, or the environment variable, or default to CLAUDE_CODE_DEFAULT_MAX_OUTPUT_TOKENS
154+
CLAUDE_CODE_MAX_OUTPUT_TOKENS:
155+
maxOutputTokens?.toString() ||
156+
process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS ||
157+
CLAUDE_CODE_DEFAULT_MAX_OUTPUT_TOKENS.toString(),
158+
}
159+
160+
// If system prompt is too long, pass it via environment variable
161+
if (useEnvForSystemPrompt) {
162+
env.CLAUDE_CODE_SYSTEM_PROMPT = systemPrompt
163+
}
164+
140165
const child = execa(claudePath, args, {
141166
stdin: "pipe",
142167
stdout: "pipe",
143168
stderr: "pipe",
144-
env: {
145-
...process.env,
146-
// Use the configured value, or the environment variable, or default to CLAUDE_CODE_DEFAULT_MAX_OUTPUT_TOKENS
147-
CLAUDE_CODE_MAX_OUTPUT_TOKENS:
148-
maxOutputTokens?.toString() ||
149-
process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS ||
150-
CLAUDE_CODE_DEFAULT_MAX_OUTPUT_TOKENS.toString(),
151-
},
169+
env,
152170
cwd,
153171
maxBuffer: 1024 * 1024 * 1000,
154172
timeout: CLAUDE_CODE_TIMEOUT,
155173
})
156174

157175
// Write messages to stdin after process is spawned
158-
// This avoids the E2BIG error on Linux when passing large messages as command line arguments
176+
// This avoids the E2BIG error on Linux and ENAMETOOLONG error on Windows when passing large data as command line arguments
159177
// Linux has a per-argument limit of ~128KiB for execve() system calls
178+
// Windows has a total command line length limit of ~8191 characters
179+
// For system prompts, we use environment variables when they exceed the safe limit
160180
const messagesJson = JSON.stringify(messages)
161181

162182
// Use setImmediate to ensure the process has been spawned before writing to stdin

0 commit comments

Comments
 (0)