Skip to content

fix: prevent E2BIG error when system prompt is too long for Claude Code CLI #976

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 85 additions & 1 deletion src/integrations/claude-code/__tests__/run.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, test, expect, vi, beforeEach } from "vitest"
import { describe, test, expect, vi, afterEach, beforeEach } from "vitest"

// Mock vscode workspace
vi.mock("vscode", () => ({
Expand Down Expand Up @@ -287,4 +287,88 @@ describe("runClaudeCode", () => {
consoleErrorSpy.mockRestore()
await generator.return(undefined)
})

describe("system prompt truncation", () => {
test("should truncate system prompt when memory bank content is detected", async () => {
const { runClaudeCode } = await import("../run")

const longSystemPrompt = `You are Claude Code, Anthropic's official CLI for Claude.
You are Kilo Code, a highly skilled software engineer.

RULES

All responses MUST follow specific guidelines.

USER'S CUSTOM INSTRUCTIONS

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.

Very long memory bank content that would cause E2BIG errors when passed as command line argument...`.repeat(100)

const options = {
systemPrompt: longSystemPrompt,
messages: [{ role: "user" as const, content: "Hello" }],
}

const generator = runClaudeCode(options)

// Since we're mocking execa, we can check what arguments were passed
await generator.next()

expect(mockExeca).toHaveBeenCalledWith(
"claude",
expect.arrayContaining([
"-p",
"--system-prompt",
expect.stringMatching(/^You are Claude Code.*RULES.*All responses MUST follow.*$/s),
]),
expect.any(Object)
)

// Verify the system prompt was truncated
const call = mockExeca.mock.calls[0]
const systemPromptArg = call[1][call[1].indexOf("--system-prompt") + 1]
expect(systemPromptArg).not.toContain("USER'S CUSTOM INSTRUCTIONS")
expect(systemPromptArg.length).toBeLessThan(longSystemPrompt.length)
})

test("should truncate system prompt when it exceeds maximum safe size", async () => {
const { runClaudeCode } = await import("../run")

const veryLongSystemPrompt = "You are a helpful assistant. ".repeat(2000) // ~60KB

const options = {
systemPrompt: veryLongSystemPrompt,
messages: [{ role: "user" as const, content: "Hello" }],
}

const generator = runClaudeCode(options)
await generator.next()

// Verify the system prompt was truncated to safe size
const call = mockExeca.mock.calls[0]
const systemPromptArg = call[1][call[1].indexOf("--system-prompt") + 1]
expect(systemPromptArg.length).toBeLessThanOrEqual(32000)
expect(systemPromptArg.length).toBeLessThan(veryLongSystemPrompt.length)
})

test("should not truncate normal-sized system prompts", async () => {
const { runClaudeCode } = await import("../run")

const normalSystemPrompt = "You are a helpful assistant."

const options = {
systemPrompt: normalSystemPrompt,
messages: [{ role: "user" as const, content: "Hello" }],
}

const generator = runClaudeCode(options)
await generator.next()

// Verify the system prompt was not truncated
const call = mockExeca.mock.calls[0]
const systemPromptArg = call[1][call[1].indexOf("--system-prompt") + 1]
expect(systemPromptArg).toBe(normalSystemPrompt)
})
})
})
57 changes: 56 additions & 1 deletion src/integrations/claude-code/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,14 @@ const CLAUDE_CODE_TIMEOUT = 600000 // 10 minutes
function runProcess({ systemPrompt, messages, path, modelId }: ClaudeCodeOptions) {
const claudePath = path || "claude"

// Truncate system prompt to avoid E2BIG errors
// Keep only the role definition part, as Claude Code CLI loads memory bank from CLAUDE.md
const truncatedSystemPrompt = truncateSystemPrompt(systemPrompt)

const args = [
"-p",
"--system-prompt",
systemPrompt,
truncatedSystemPrompt,
"--verbose",
"--output-format",
"stream-json",
Expand Down Expand Up @@ -198,3 +202,54 @@ function attemptParseChunk(data: string): ClaudeCodeMessage | null {
return null
}
}

/**
* Truncates the system prompt to avoid E2BIG errors when spawning subprocess.
* Keeps only the role definition part since Claude Code CLI loads memory bank from CLAUDE.md.
*
* The system prompt structure typically follows:
* 1. Role definition (essential)
* 2. Memory bank instructions with content (can be large, not needed for Claude Code CLI)
*
* @param systemPrompt The full system prompt
* @returns Truncated system prompt containing only the role definition
*/
function truncateSystemPrompt(systemPrompt: string): string {
// Look for common patterns that indicate the start of memory bank content
const memoryBankIndicators = [
"USER'S CUSTOM INSTRUCTIONS",
"The following additional instructions are provided by the user",
"# Rules from",
"# Memory Bank",
"CLAUDE.md",
"memory bank",
"custom instructions"
]

// Find the first occurrence of any memory bank indicator
let truncationPoint = systemPrompt.length

for (const indicator of memoryBankIndicators) {
const index = systemPrompt.indexOf(indicator)
if (index !== -1 && index < truncationPoint) {
truncationPoint = index
}
}

// If we found a truncation point, cut off there
if (truncationPoint < systemPrompt.length) {
const truncated = systemPrompt.substring(0, truncationPoint).trim()
console.log(`System prompt truncated from ${systemPrompt.length} to ${truncated.length} characters`)
return truncated
}

// If no memory bank content found, but still very long, truncate to reasonable size
const MAX_SAFE_SIZE = 32000 // Conservative limit to avoid E2BIG
if (systemPrompt.length > MAX_SAFE_SIZE) {
const truncated = systemPrompt.substring(0, MAX_SAFE_SIZE).trim()
console.log(`System prompt truncated from ${systemPrompt.length} to ${truncated.length} characters (size limit)`)
return truncated
}

return systemPrompt
}