Skip to content

session-lifecycle-hook: SessionStart appends env exports without dedup - env file grows unboundedly on resume/compact, breaking Bash on Windows (8191-char limit) #528

Description

@tfukahori

Summary

scripts/session-lifecycle-hook.mjs appends its three export lines to CLAUDE_ENV_FILE on every SessionStart event without checking whether they are already present. Claude Code re-fires SessionStart on every session resume and compaction, so in a long-running session the env file grows without bound.

Because Claude Code prepends the entire env file to every Bash tool invocation, the accumulated duplicates eventually push the composed bash -c string past the Windows 8191-character command-line limit. The command gets truncated mid-token, and every Bash call longer than a few hundred bytes fails with:

/usr/bin/bash: -c: line 70: unexpected EOF while looking for matching `''

The failures look random (long gh pr create commands, heredocs, etc. fail; short commands succeed), which makes this very hard to diagnose from the outside.

Observed impact (real session, plugin v1.0.6, Windows 11)

  • ~/.claude/session-env/<session-id>/sessionstart-hook-1.sh contained the same 3 export lines duplicated 23× (~7.4 KB) after a long session with many compactions/resumes.
  • Measured via echo ${#BASH_EXECUTION_STRING}: a 103-character command produced an 8042-character bash -c string (lines 1–69 of it were the duplicated exports).
  • Remaining budget for the actual command was ~250 characters; anything longer was truncated at the 8191-char boundary → shell syntax error.
  • Bisected empirically: an all-ASCII true "AAA…" probe passed at ~360 total bytes and failed at ~400, consistent with the 8191 limit given the ~7.9 KB prefix.

Root cause

appendEnvVar() in scripts/session-lifecycle-hook.mjs:

function appendEnvVar(name, value) {
  if (!process.env.CLAUDE_ENV_FILE || value == null || value === "") {
    return;
  }
  fs.appendFileSync(process.env.CLAUDE_ENV_FILE, `export ${name}=${shellEscape(value)}\n`, "utf8");
}

handleSessionStart() calls this for CODEX_COMPANION_SESSION_ID, CODEX_COMPANION_TRANSCRIPT_PATH, and CLAUDE_PLUGIN_DATA on each SessionStart — including source: "resume" / "compact" re-fires — with no dedup.

Suggested fix (verified locally)

Make the append idempotent:

function appendEnvVar(name, value) {
  if (!process.env.CLAUDE_ENV_FILE || value == null || value === "") {
    return;
  }
  const line = `export ${name}=${shellEscape(value)}\n`;
  try {
    if (fs.readFileSync(process.env.CLAUDE_ENV_FILE, "utf8").includes(line)) {
      return;
    }
  } catch {
    // file may not exist yet; fall through to append
  }
  fs.appendFileSync(process.env.CLAUDE_ENV_FILE, line, "utf8");
}

Verified by invoking the hook three times with the same SessionStart payload against a scratch CLAUDE_ENV_FILE: the file stays at exactly 3 lines (unpatched: 9).

An alternative would be to rewrite the file wholesale on each SessionStart (the three values are session-scoped constants), which also self-heals files already bloated by previous plugin versions.

Environment

  • Plugin: codex@openai-codex 1.0.6 (marketplace refreshed; 1.0.6 is current latest)
  • Claude Code on Windows 11 (Git Bash as the Bash tool shell — the 8191-char limit is what turns the unbounded growth into hard failures, but the growth itself is platform-independent)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions