Skip to content

inference() passes the system prompt via argv → E2BIG on large system prompts (>128 KiB) #1534

Description

@christauff

Issue: inference() passes the system prompt via argv → E2BIG on large system prompts

Repo: danielmiessler/LifeOS · File: LifeOS/install/LIFEOS/TOOLS/Inference.ts (~l.216–225) — confirm line refs against the live file
Version seen: current main (last touched in 7.0.0, 2026-07-12)

Thanks again — offered as contributor support. This is a small companion to a fix you already made (verified by reproduction on Linux, claude 2.1.214).

Summary

inference() builds the claude argv with '--system-prompt', options.systemPrompt. A single argv element cannot exceed MAX_ARG_STRLEN = 32 × PAGE_SIZE = 131072 bytes (128 KiB) on Linux — this is a per-argument kernel limit, distinct from and much smaller than the ~2 MB total ARG_MAX, and it is not tunable via ulimit. So a system prompt larger than ~128 KiB overflows and the spawn fails with E2BIG: argument list too long (fatal for that call).

Verified on this box: a single arg of 100 KB succeeds; 128 KB, 200 KB, 512 KB, 2 MB all fail with E2BIG.

Notably the code already solved this for the user prompt — the l.247 comment "Write prompt via stdin to avoid ARG_MAX limits on large inputs" moved the user prompt to stdin. The system prompt was left on argv, so a large one still overflows.

Reproduction

Call inference() with a systemPrompt > ~128 KiB (e.g. a large formation/memory context). Result:

E2BIG: argument list too long, posix_spawn '.../claude'

Current code (~l.216–225)

const args = [ '--print', '--model', model, '--effort', config.effort, ...,
  '--system-prompt', options.systemPrompt ];   // <-- fails at ~128 KiB (MAX_ARG_STRLEN)

Proposed fix (tested)

claude supports --system-prompt-file (verified present in 2.1.214; it's the replace-family counterpart of --system-prompt). Route large prompts through a temp file, gating on bytes well under 128 KiB, with a collision-safe name and idempotent cleanup on all exit paths:

import { writeFileSync, unlinkSync } from 'fs';
import { tmpdir } from 'os';
import { randomUUID } from 'crypto';

const SYS_ARGV_LIMIT = 100_000; // bytes; MAX_ARG_STRLEN=131072 — stay under, and count BYTES not chars
let sysTmpFile: string | null = null;
let systemPromptArgs: string[];
if (Buffer.byteLength(options.systemPrompt, 'utf8') > SYS_ARGV_LIMIT) {
  sysTmpFile = join(tmpdir(), `lifeos-sysprompt-${process.pid}-${randomUUID()}.txt`); // randomUUID: parallel inference() calls in one PID collide otherwise
  writeFileSync(sysTmpFile, options.systemPrompt, { encoding: 'utf8', mode: 0o600 }); // 0600: system prompt can carry sensitive context
  systemPromptArgs = ['--system-prompt-file', sysTmpFile];
} else {
  systemPromptArgs = ['--system-prompt', options.systemPrompt];
}
let tmpCleaned = false;
const cleanupTmp = () => { if (tmpCleaned || !sysTmpFile) return; tmpCleaned = true; try { unlinkSync(sysTmpFile); } catch {} };
// args: replace the '--system-prompt' line with ...systemPromptArgs
// call cleanupTmp() in proc 'close', the timeout handler, AND proc 'error' (spawn failure leaks otherwise)

Why these details matter (all reproduced): a naive > 500_000 char threshold leaves 128 KB–500 KB prompts still crashing; .length counts UTF-16 units so multibyte content underestimates bytes; a pid+Date.now() filename collides across parallel inference() calls (the cross-model runner does exactly this); and close+timeout alone double-unlinks and leaks on spawn-error. Verified: a 200 KB system prompt (previously in the crash band) now succeeds, no temp-file leak.

Happy to open a PR (mirrors the fix already applied to the user prompt).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions