Skip to content

fix(runner): pass appended system prompt via file to avoid Windows argv limit (ENAMETOOLONG) - #251

Open
hoangqnguyen wants to merge 1 commit into
moazbuilds:masterfrom
hoangqnguyen:fix/prompt-file-avoid-cmdline-limit
Open

fix(runner): pass appended system prompt via file to avoid Windows argv limit (ENAMETOOLONG)#251
hoangqnguyen wants to merge 1 commit into
moazbuilds:masterfrom
hoangqnguyen:fix/prompt-file-avoid-cmdline-limit

Conversation

@hoangqnguyen

Copy link
Copy Markdown
Contributor

Problem

On Windows, users hit a raw ENAMETOOLONG: name too long, uv_spawn error (surfaced in Telegram/Discord) instead of a reply.

The daemon passes the appended system prompt inline on every spawn:

// src/runner.ts (main, fallback, and streaming spawn paths)
args.push("--append-system-prompt", appendParts.join("\n\n"));

appendParts includes the project CLAUDE.md plus the directory-scoping and untrusted-content wrappers — easily 25+ KB. Windows caps a process command line (CreateProcessW) at 32,767 chars, so:

  • a large CLAUDE.md alone can exceed it, or
  • a normal-size CLAUDE.md plus a long user message can push the total over.

Either way Bun.spawn fails with ENAMETOOLONG before Claude ever runs, and the raw error is shown to the user.

Fix

Write the append to a temp file and pass it via --append-system-prompt-file instead of inline. This keeps the ~25 KB off the command line, so only the (short) user prompt and flags remain on argv. The append size no longer contributes to the argv limit at all — neither a big CLAUDE.md nor a long message can trigger the failure.

args.push("--append-system-prompt-file", writeAppendPromptFile(appendParts.join("\n\n")));
  • writeAppendPromptFile() writes under the project's own .claude/claudeclaw/tmp (same trust scope as CLAUDE.md) and sweeps files older than 10 min on each call so they don't accumulate — no explicit per-spawn cleanup plumbing, and it's crash-safe.
  • Applied to the main, fallback, and streaming spawn paths. The small, fixed fork system prompt stays inline (never near the limit).
  • Harmless on Linux/macOS (much higher ARG_MAX) — the file path is simply always used.

Testing

  • Verified against the real claude CLI: a 43.5 KB append fails inline with Argument list too long, but succeeds via --append-system-prompt-file ({"subtype":"success","is_error":false}). --append-system-prompt-file is an existing CLI flag.
  • bunx tsc --noEmit clean.
  • bun test: only the pre-existing sessionFiles/sessions POSIX-path tests fail on a Windows dev box — unrelated to this change.

Notes

…gv limit

The daemon passes the appended system prompt (CLAUDE.md plus scoping/safety
wrappers — easily 25+ KB) inline via --append-system-prompt on every spawn.
Combined with the user's message, the command line can exceed Windows'
32767-char CreateProcessW limit, and Bun.spawn fails with ENAMETOOLONG
before Claude ever starts. Users saw a raw "ENAMETOOLONG: name too long,
uv_spawn" error instead of a reply.

Write the append to a temp file and pass it via --append-system-prompt-file
instead, keeping that bulk off the command line so only the (short) user
prompt and flags remain on argv. The append size no longer contributes to
the limit at all, so neither a large CLAUDE.md nor a long user message can
trigger the failure.

Temp files live under the project's own .claude/claudeclaw/tmp (same trust
scope as CLAUDE.md) and are swept (>10 min old) on each call so they don't
accumulate. Applied to the main, fallback, and streaming spawn paths; the
small fixed fork system prompt stays inline.

Verified against claude.exe: a 43.5 KB append fails inline with "Argument
list too long" but succeeds via --append-system-prompt-file. tsc --noEmit
clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@TerrysPOV TerrysPOV left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good fix, and verified sound: --append-system-prompt-file is a real flag (confirmed against claude CLI 2.1.215), so moving the append bulk off argv is a valid way to keep the command line under the Windows CreateProcessW 32767-char cap. The helper writes a uniquely-named temp file under .claude/claudeclaw/tmp (same trust scope as CLAUDE.md), sweeps entries older than 10 min, and all three large-append call sites are converted consistently.

Worth calling out: this is complementary to what's already on master, not a duplicate. Commit 87dcd7f ("bypass claude.cmd on Windows to avoid 8K command-line limit") already lifts the ceiling from cmd.exe's 8191 to CreateProcessW's 32767 by resolving claude.exe directly; this PR handles the residual case where a large CLAUDE.md + long message can overflow even 32767. The two stack correctly.

Code review

Two things before merge:

  1. src/runner.ts genuinely conflicts with master (not just the version files) — the branch was cut from an older base. The conflicts are mechanical: the fs import line collides with master's added hasValidSessionId import from #234 (resolve = keep both), plus line drift on the --append-system-prompt call sites. Please rebase onto current master and resolve, and re-confirm on Windows since this touches the same spawn path as 87dcd7f.

claudeclaw/src/runner.ts

Lines 88 to 104 in 9c86cc0

*/
const APPEND_TMP_DIR = join(process.cwd(), ".claude", "claudeclaw", "tmp");
let appendFileSeq = 0;
function writeAppendPromptFile(content: string): string {
try {
mkdirSync(APPEND_TMP_DIR, { recursive: true });
const cutoff = Date.now() - 10 * 60_000;
for (const f of readdirSync(APPEND_TMP_DIR)) {
try {
const p = join(APPEND_TMP_DIR, f);
if (statSync(p).mtimeMs < cutoff) rmSync(p, { force: true });
} catch {}
}
} catch {}
const file = join(APPEND_TMP_DIR, `append-${process.pid}-${Date.now()}-${appendFileSeq++}.txt`);
writeFileSync(file, content, "utf8");
return file;

  1. Version is stale — this targets 1.0.41 but master is 1.0.43. Bump both .claude-plugin/*.json to 1.0.44 as part of the rebase.

Non-blocking notes:

  • The just-written append file isn't unlinked after the run; cleanup relies on the next call's >10-min sweep, so files linger briefly. Bounded and fine, but a try { rmSync(file) } finally after the spawn returns would keep the dir empty.
  • On the fallback path both the primary and fallback build an append file, so the primary's file is orphaned when fallback is taken (swept later). Minor waste.
  • The fork spawn still uses --append-system-prompt FORK_SYSTEM_PROMPT (runner.ts ~1740) rather than the file form — which is correct (it's a short fixed constant, nowhere near the limit); just noting it's intentional and consistent to leave it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants