Skip to content

Commit 45298df

Browse files
committed
fix: prevent E2BIG error when system prompt is too long for Claude Code CLI
- Add system prompt truncation to avoid E2BIG errors - Detect memory bank content patterns and truncate before command-line args - Preserve essential role definition while removing large memory bank content - Add comprehensive tests for truncation scenarios - Claude Code CLI loads memory bank from CLAUDE.md anyway, so this is safe Fixes issue where very long system prompts (especially with memory bank content) would cause subprocess spawn failures due to command-line argument size limits.
1 parent c25136c commit 45298df

File tree

2 files changed

+141
-2
lines changed

2 files changed

+141
-2
lines changed

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

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, test, expect, vi, beforeEach } from "vitest"
1+
import { describe, test, expect, vi, afterEach, beforeEach } from "vitest"
22

33
// Mock vscode workspace
44
vi.mock("vscode", () => ({
@@ -287,4 +287,88 @@ describe("runClaudeCode", () => {
287287
consoleErrorSpy.mockRestore()
288288
await generator.return(undefined)
289289
})
290+
291+
describe("system prompt truncation", () => {
292+
test("should truncate system prompt when memory bank content is detected", async () => {
293+
const { runClaudeCode } = await import("../run")
294+
295+
const longSystemPrompt = `You are Claude Code, Anthropic's official CLI for Claude.
296+
You are Kilo Code, a highly skilled software engineer.
297+
298+
RULES
299+
300+
All responses MUST follow specific guidelines.
301+
302+
USER'S CUSTOM INSTRUCTIONS
303+
304+
The following additional instructions are provided by the user, and should be followed to the best of your ability without interfering with the TOOL USE guidelines.
305+
306+
Very long memory bank content that would cause E2BIG errors when passed as command line argument...`.repeat(100)
307+
308+
const options = {
309+
systemPrompt: longSystemPrompt,
310+
messages: [{ role: "user" as const, content: "Hello" }],
311+
}
312+
313+
const generator = runClaudeCode(options)
314+
315+
// Since we're mocking execa, we can check what arguments were passed
316+
await generator.next()
317+
318+
expect(mockExeca).toHaveBeenCalledWith(
319+
"claude",
320+
expect.arrayContaining([
321+
"-p",
322+
"--system-prompt",
323+
expect.stringMatching(/^You are Claude Code.*RULES.*All responses MUST follow.*$/s),
324+
]),
325+
expect.any(Object)
326+
)
327+
328+
// Verify the system prompt was truncated
329+
const call = mockExeca.mock.calls[0]
330+
const systemPromptArg = call[1][call[1].indexOf("--system-prompt") + 1]
331+
expect(systemPromptArg).not.toContain("USER'S CUSTOM INSTRUCTIONS")
332+
expect(systemPromptArg.length).toBeLessThan(longSystemPrompt.length)
333+
})
334+
335+
test("should truncate system prompt when it exceeds maximum safe size", async () => {
336+
const { runClaudeCode } = await import("../run")
337+
338+
const veryLongSystemPrompt = "You are a helpful assistant. ".repeat(2000) // ~60KB
339+
340+
const options = {
341+
systemPrompt: veryLongSystemPrompt,
342+
messages: [{ role: "user" as const, content: "Hello" }],
343+
}
344+
345+
const generator = runClaudeCode(options)
346+
await generator.next()
347+
348+
// Verify the system prompt was truncated to safe size
349+
const call = mockExeca.mock.calls[0]
350+
const systemPromptArg = call[1][call[1].indexOf("--system-prompt") + 1]
351+
expect(systemPromptArg.length).toBeLessThanOrEqual(32000)
352+
expect(systemPromptArg.length).toBeLessThan(veryLongSystemPrompt.length)
353+
})
354+
355+
test("should not truncate normal-sized system prompts", async () => {
356+
const { runClaudeCode } = await import("../run")
357+
358+
const normalSystemPrompt = "You are a helpful assistant."
359+
360+
const options = {
361+
systemPrompt: normalSystemPrompt,
362+
messages: [{ role: "user" as const, content: "Hello" }],
363+
}
364+
365+
const generator = runClaudeCode(options)
366+
await generator.next()
367+
368+
// Verify the system prompt was not truncated
369+
const call = mockExeca.mock.calls[0]
370+
const systemPromptArg = call[1][call[1].indexOf("--system-prompt") + 1]
371+
expect(systemPromptArg).toBe(normalSystemPrompt)
372+
})
373+
})
290374
})

src/integrations/claude-code/run.ts

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,10 +110,14 @@ const CLAUDE_CODE_TIMEOUT = 600000 // 10 minutes
110110
function runProcess({ systemPrompt, messages, path, modelId }: ClaudeCodeOptions) {
111111
const claudePath = path || "claude"
112112

113+
// Truncate system prompt to avoid E2BIG errors
114+
// Keep only the role definition part, as Claude Code CLI loads memory bank from CLAUDE.md
115+
const truncatedSystemPrompt = truncateSystemPrompt(systemPrompt)
116+
113117
const args = [
114118
"-p",
115119
"--system-prompt",
116-
systemPrompt,
120+
truncatedSystemPrompt,
117121
"--verbose",
118122
"--output-format",
119123
"stream-json",
@@ -198,3 +202,54 @@ function attemptParseChunk(data: string): ClaudeCodeMessage | null {
198202
return null
199203
}
200204
}
205+
206+
/**
207+
* Truncates the system prompt to avoid E2BIG errors when spawning subprocess.
208+
* Keeps only the role definition part since Claude Code CLI loads memory bank from CLAUDE.md.
209+
*
210+
* The system prompt structure typically follows:
211+
* 1. Role definition (essential)
212+
* 2. Memory bank instructions with content (can be large, not needed for Claude Code CLI)
213+
*
214+
* @param systemPrompt The full system prompt
215+
* @returns Truncated system prompt containing only the role definition
216+
*/
217+
function truncateSystemPrompt(systemPrompt: string): string {
218+
// Look for common patterns that indicate the start of memory bank content
219+
const memoryBankIndicators = [
220+
"USER'S CUSTOM INSTRUCTIONS",
221+
"The following additional instructions are provided by the user",
222+
"# Rules from",
223+
"# Memory Bank",
224+
"CLAUDE.md",
225+
"memory bank",
226+
"custom instructions"
227+
]
228+
229+
// Find the first occurrence of any memory bank indicator
230+
let truncationPoint = systemPrompt.length
231+
232+
for (const indicator of memoryBankIndicators) {
233+
const index = systemPrompt.indexOf(indicator)
234+
if (index !== -1 && index < truncationPoint) {
235+
truncationPoint = index
236+
}
237+
}
238+
239+
// If we found a truncation point, cut off there
240+
if (truncationPoint < systemPrompt.length) {
241+
const truncated = systemPrompt.substring(0, truncationPoint).trim()
242+
console.log(`System prompt truncated from ${systemPrompt.length} to ${truncated.length} characters`)
243+
return truncated
244+
}
245+
246+
// If no memory bank content found, but still very long, truncate to reasonable size
247+
const MAX_SAFE_SIZE = 32000 // Conservative limit to avoid E2BIG
248+
if (systemPrompt.length > MAX_SAFE_SIZE) {
249+
const truncated = systemPrompt.substring(0, MAX_SAFE_SIZE).trim()
250+
console.log(`System prompt truncated from ${systemPrompt.length} to ${truncated.length} characters (size limit)`)
251+
return truncated
252+
}
253+
254+
return systemPrompt
255+
}

0 commit comments

Comments
 (0)