fix(runner): pass appended system prompt via file to avoid Windows argv limit (ENAMETOOLONG) - #251
Conversation
…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
left a comment
There was a problem hiding this comment.
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:
src/runner.tsgenuinely conflicts with master (not just the version files) — the branch was cut from an older base. The conflicts are mechanical: thefsimport line collides with master's addedhasValidSessionIdimport from #234 (resolve = keep both), plus line drift on the--append-system-promptcall sites. Please rebase onto current master and resolve, and re-confirm on Windows since this touches the same spawn path as87dcd7f.
Lines 88 to 104 in 9c86cc0
- Version is stale — this targets 1.0.41 but master is 1.0.43. Bump both
.claude-plugin/*.jsonto 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) } finallyafter 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.
Problem
On Windows, users hit a raw
ENAMETOOLONG: name too long, uv_spawnerror (surfaced in Telegram/Discord) instead of a reply.The daemon passes the appended system prompt inline on every spawn:
appendPartsincludes the projectCLAUDE.mdplus the directory-scoping and untrusted-content wrappers — easily 25+ KB. Windows caps a process command line (CreateProcessW) at 32,767 chars, so:CLAUDE.mdalone can exceed it, orCLAUDE.mdplus a long user message can push the total over.Either way
Bun.spawnfails withENAMETOOLONGbefore 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-fileinstead 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 bigCLAUDE.mdnor a long message can trigger the failure.writeAppendPromptFile()writes under the project's own.claude/claudeclaw/tmp(same trust scope asCLAUDE.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.ARG_MAX) — the file path is simply always used.Testing
claudeCLI: a 43.5 KB append fails inline withArgument list too long, but succeeds via--append-system-prompt-file({"subtype":"success","is_error":false}).--append-system-prompt-fileis an existing CLI flag.bunx tsc --noEmitclean.bun test: only the pre-existingsessionFiles/sessionsPOSIX-path tests fail on a Windows dev box — unrelated to this change.Notes
1.0.42to avoid clashing with fix(telegram): queue messages received while Claude is busy instead of dropping them #250's1.0.41— happy to rebase/renumber if you'd prefer.