Skip to content

SessionStart hook appends duplicate exports to CLAUDE_ENV_FILE on every start, resume and compaction, so every Bash call fails with E2BIG in long sessions #783

Open

Description

@mbudge

Summary

scripts/session-lifecycle-hook.mjs appends three export lines to $CLAUDE_ENV_FILE each time SessionStart fires, with no de-duplication. Claude Code fires SessionStart again, under the same session id, on resume and after every compaction. In a long session the file therefore fills with hundreds of identical copies.

Claude Code inlines that env file into the single bash -c argument of every Bash command. Once the argument passes Linux's 128 KiB per-argument limit (MAX_ARG_STRLEN), every Bash and Monitor call fails before it starts, and so does every sub-agent's shell. The session loses its shell completely, and restarting does not recover it, because resuming re-reads the bloated file and adds three more lines.

Environment

  • codex plugin 1.0.6 (codex@openai-codex, from openai/codex-plugin-cc)
  • Claude Code 2.1.281
  • Linux 7.0.0-29-generic, GNU bash 5.3.9
  • getconf ARG_MAX = 2097152. The limit hit is the 128 KiB per-argument cap, not ARG_MAX.

Evidence

1. The error. Every Bash call failed with this, including echo ok:

Could not start /bin/bash: the command line plus environment exceed the OS exec argument limit (E2BIG).
At spawn: command line 133KB across 3 args (largest single arg 133KB); environment 3.3KB across 47 vars (largest: LS_COLORS at 1.7KB).

The environment is only 3.3 KB. Almost all of the ~133 KB is the one bash -c argument.

2. The argument grew steadily through the session. Sizes from successive errors: 128.1 → 128.2 → 129.1 → 131.8 → 131.9 → 132.1 → 132.8 → 133.0 → 133.3 → 133.5 KB. At first only long commands failed; once the base passed 128 KiB, every command did. The jump over a restart and resume (133.0 → 133.3 KB, about 0.3 KB) matches one appended block of three lines (~311 bytes).

3. The env file held the same three lines, repeated. ~/.claude/session-env/<session-id>/sessionstart-hook-0.sh:

export CODEX_COMPANION_SESSION_ID='<session-id>'
export CODEX_COMPANION_TRANSCRIPT_PATH='~/.claude/projects/<project>/<session-id>.jsonl'
export CLAUDE_PLUGIN_DATA='~/.claude/plugins/data/codex-openai-codex'
export CODEX_COMPANION_SESSION_ID='<session-id>'
export CODEX_COMPANION_TRANSCRIPT_PATH='~/.claude/projects/<project>/<session-id>.jsonl'
export CLAUDE_PLUGIN_DATA='~/.claude/plugins/data/codex-openai-codex'
... (the same triplet, over and over)

The session transcript has 136 compaction boundaries, and each is followed by a SessionStart.

4. Trimming the file fixed it. I reduced the file to one triplet (3 lines) and restarted with claude --resume. echo ok then succeeded.

5. It keeps growing after the fix. Shortly after the trim, the file already held 9 lines, 3 copies of each variable (sort | uniq -c):

      3 export CLAUDE_PLUGIN_DATA='~/.claude/plugins/data/codex-openai-codex'
      3 export CODEX_COMPANION_SESSION_ID='<session-id>'
      3 export CODEX_COMPANION_TRANSCRIPT_PATH='~/.claude/projects/<project>/<session-id>.jsonl'

6. Causes ruled out. Disabling the sandbox for the call, deleting large scratch directories, and a plain restart each left the argument size unchanged. No settings file configures a sandbox or deny list, and the user's .bashrc/.profile are stock.

Root cause

scripts/session-lifecycle-hook.mjs:

  • lines 35-40: appendEnvVar() calls fs.appendFileSync without de-duplicating;
  • lines 77-81: handleSessionStart() calls it three times on every SessionStart, whatever its source.

Steps to reproduce

  1. Enable the codex plugin.
  2. Start a session, then resume it (claude --resume) or let it compact, several times.
  3. Run sort ~/.claude/session-env/<session-id>/sessionstart-hook-0.sh | uniq -c. The count goes up by one per SessionStart.
  4. At about 420 copies (~128 KiB), every Bash call fails with E2BIG.

Expected

Each variable is exported once per session, whatever number of times SessionStart fires.

Suggested fix

function appendEnvVar(name, value) {
  const file = process.env.CLAUDE_ENV_FILE;
  if (!file || value == null || value === "") {
    return;
  }
  const line = `export ${name}=${shellEscape(value)}`;
  const existing = fs.existsSync(file) ? fs.readFileSync(file, "utf8").split("\n") : [];
  if (existing.includes(line)) {
    return;
  }
  fs.appendFileSync(file, `${line}\n`, "utf8");
}

An alternative is to export only when the hook input's source is startup. De-duplicating is more robust, because it also covers a changed value on resume.

Workaround

Overwrite ~/.claude/session-env/<session-id>/sessionstart-hook-0.sh with one copy of the three lines, then restart Claude Code and resume the session. The file is read at session start.

Activity

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

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