Skip to content

Commit 94d6d69

Browse files
committed
fix: prevent E2BIG error by using stdin for large system prompts
- Add MAX_CMDLINE_ARG_SIZE constant (100KB) to detect large system prompts - Automatically use stdin instead of --system-prompt flag when prompt exceeds threshold - Maintains backward compatibility for small prompts on non-Windows platforms - Extends existing Windows behavior to Linux when needed - Add comprehensive tests for system prompt size detection logic Fixes #6436
1 parent 6f9fea3 commit 94d6d69

File tree

2 files changed

+145
-6
lines changed

2 files changed

+145
-6
lines changed

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

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,4 +289,135 @@ describe("runClaudeCode", () => {
289289
consoleErrorSpy.mockRestore()
290290
await generator.return(undefined)
291291
})
292+
293+
test("should use stdin for large system prompts on non-Windows platforms", async () => {
294+
const { runClaudeCode } = await import("../run")
295+
const os = await import("os")
296+
vi.mocked(os.platform).mockReturnValue("linux")
297+
298+
// Create a large system prompt (over 100KB)
299+
const largeSystemPrompt = "A".repeat(101 * 1024) // 101KB
300+
const messages = [{ role: "user" as const, content: "Hello" }]
301+
const options = {
302+
systemPrompt: largeSystemPrompt,
303+
messages,
304+
}
305+
306+
const generator = runClaudeCode(options)
307+
308+
// Consume at least one item to trigger process spawn
309+
await generator.next()
310+
311+
// On non-Windows with large system prompt, should NOT have --system-prompt in args
312+
const [, args] = mockExeca.mock.calls[0]
313+
expect(args).not.toContain("--system-prompt")
314+
expect(args).not.toContain(largeSystemPrompt)
315+
316+
// Should pass both system prompt and messages via stdin (like Windows)
317+
const expectedStdinData = JSON.stringify({ systemPrompt: largeSystemPrompt, messages })
318+
expect(mockStdin.write).toHaveBeenCalledWith(expectedStdinData, "utf8", expect.any(Function))
319+
320+
// Clean up
321+
await generator.return(undefined)
322+
})
323+
324+
test("should use command line args for small system prompts on non-Windows platforms", async () => {
325+
const { runClaudeCode } = await import("../run")
326+
const os = await import("os")
327+
vi.mocked(os.platform).mockReturnValue("linux")
328+
329+
// Create a small system prompt (under 100KB)
330+
const smallSystemPrompt = "You are a helpful assistant"
331+
const messages = [{ role: "user" as const, content: "Hello" }]
332+
const options = {
333+
systemPrompt: smallSystemPrompt,
334+
messages,
335+
}
336+
337+
const generator = runClaudeCode(options)
338+
339+
// Consume at least one item to trigger process spawn
340+
await generator.next()
341+
342+
// On non-Windows with small system prompt, should have --system-prompt in args
343+
const [, args] = mockExeca.mock.calls[0]
344+
expect(args).toContain("--system-prompt")
345+
expect(args).toContain(smallSystemPrompt)
346+
347+
// Should only pass messages via stdin
348+
expect(mockStdin.write).toHaveBeenCalledWith(JSON.stringify(messages), "utf8", expect.any(Function))
349+
350+
// Clean up
351+
await generator.return(undefined)
352+
})
353+
354+
test("should always use stdin for system prompts on Windows regardless of size", async () => {
355+
const { runClaudeCode } = await import("../run")
356+
const os = await import("os")
357+
vi.mocked(os.platform).mockReturnValue("win32")
358+
359+
// Test with both small and large system prompts
360+
const testCases = [
361+
{ name: "small", systemPrompt: "You are a helpful assistant" },
362+
{ name: "large", systemPrompt: "A".repeat(101 * 1024) }, // 101KB
363+
]
364+
365+
for (const testCase of testCases) {
366+
vi.clearAllMocks()
367+
mockExeca.mockReturnValue(createMockProcess())
368+
369+
const messages = [{ role: "user" as const, content: "Hello" }]
370+
const options = {
371+
systemPrompt: testCase.systemPrompt,
372+
messages,
373+
}
374+
375+
const generator = runClaudeCode(options)
376+
377+
// Consume at least one item to trigger process spawn
378+
await generator.next()
379+
380+
// On Windows, should never have --system-prompt in args regardless of size
381+
const [, args] = mockExeca.mock.calls[0]
382+
expect(args).not.toContain("--system-prompt")
383+
expect(args).not.toContain(testCase.systemPrompt)
384+
385+
// Should always pass both system prompt and messages via stdin
386+
const expectedStdinData = JSON.stringify({ systemPrompt: testCase.systemPrompt, messages })
387+
expect(mockStdin.write).toHaveBeenCalledWith(expectedStdinData, "utf8", expect.any(Function))
388+
389+
// Clean up
390+
await generator.return(undefined)
391+
}
392+
})
393+
394+
test("should handle edge case system prompt at exactly 100KB threshold", async () => {
395+
const { runClaudeCode } = await import("../run")
396+
const os = await import("os")
397+
vi.mocked(os.platform).mockReturnValue("linux")
398+
399+
// Create a system prompt at exactly 100KB
400+
const exactThresholdPrompt = "A".repeat(100 * 1024) // Exactly 100KB
401+
const messages = [{ role: "user" as const, content: "Hello" }]
402+
const options = {
403+
systemPrompt: exactThresholdPrompt,
404+
messages,
405+
}
406+
407+
const generator = runClaudeCode(options)
408+
409+
// Consume at least one item to trigger process spawn
410+
await generator.next()
411+
412+
// At exactly 100KB, should still use command line args (threshold is exclusive)
413+
const [, args] = mockExeca.mock.calls[0]
414+
expect(args).toContain("--system-prompt")
415+
expect(args).toContain(exactThresholdPrompt)
416+
417+
// Should only pass messages via stdin
418+
expect(mockStdin.write).toHaveBeenCalledWith(JSON.stringify(messages), "utf8", expect.any(Function))
419+
420+
// Clean up
421+
await generator.return(undefined)
422+
})
292423
})

src/integrations/claude-code/run.ts

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

112112
const CLAUDE_CODE_TIMEOUT = 600000 // 10 minutes
113113

114+
// Maximum safe size for command line arguments to avoid E2BIG error on Linux (~128KB limit)
115+
// We use 100KB as a conservative threshold to account for other arguments and shell overhead
116+
const MAX_CMDLINE_ARG_SIZE = 100 * 1024 // 100KB
117+
114118
function runProcess({
115119
systemPrompt,
116120
messages,
@@ -121,11 +125,15 @@ function runProcess({
121125
const claudePath = path || "claude"
122126
const isWindows = os.platform() === "win32"
123127

124-
// Build args based on platform
128+
// Check if system prompt is too large for command line arguments
129+
const systemPromptSize = Buffer.byteLength(systemPrompt, "utf8")
130+
const useStdinForSystemPrompt = isWindows || systemPromptSize > MAX_CMDLINE_ARG_SIZE
131+
132+
// Build args based on platform and system prompt size
125133
const args = ["-p"]
126134

127-
// Pass system prompt as flag on non-Windows, via stdin on Windows (avoids cmd length limits)
128-
if (!isWindows) {
135+
// Pass system prompt as flag only if it's small enough and not on Windows
136+
if (!useStdinForSystemPrompt) {
129137
args.push("--system-prompt", systemPrompt)
130138
}
131139

@@ -161,10 +169,10 @@ function runProcess({
161169
timeout: CLAUDE_CODE_TIMEOUT,
162170
})
163171

164-
// Prepare stdin data: Windows gets both system prompt & messages (avoids 8191 char limit),
165-
// other platforms get messages only (avoids Linux E2BIG error from ~128KiB execve limit)
172+
// Prepare stdin data: Windows and large system prompts get both system prompt & messages,
173+
// other platforms with small prompts get messages only (avoids Linux E2BIG error from ~128KiB execve limit)
166174
let stdinData: string
167-
if (isWindows) {
175+
if (useStdinForSystemPrompt) {
168176
stdinData = JSON.stringify({
169177
systemPrompt,
170178
messages,

0 commit comments

Comments
 (0)