Repository navigation
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
Description
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, fromopenai/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()callsfs.appendFileSyncwithout de-duplicating; - lines 77-81:
handleSessionStart()calls it three times on everySessionStart, whatever itssource.
Steps to reproduce
- Enable the codex plugin.
- Start a session, then resume it (
claude --resume) or let it compact, several times. - Run
sort ~/.claude/session-env/<session-id>/sessionstart-hook-0.sh | uniq -c. The count goes up by one perSessionStart. - 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