SessionStart hook appends duplicate exports to CLAUDE_ENV_FILE until every Bash call fails
Plugin: codex 1.0.5 (marketplace openai-codex, commit 80c31f9)
Platform: Windows 10 Pro 19045, Claude Code, Git Bash (/usr/bin/bash)
Summary
handleSessionStart appends three export lines to $CLAUDE_ENV_FILE unconditionally. SessionStart fires again on every resume, clear, and compact, so a long-lived session accumulates the same three lines over and over. Claude Code inlines that file into the bash -c prelude for every Bash tool call, and the prelude is truncated past roughly 8 KB. The cut lands mid-string inside a quoted export, so from that point on every Bash invocation in that session fails:
/usr/bin/bash: -c: line 84: unexpected EOF while looking for matching `''
echo ok included. The failure is total and permanent for the affected session — the truncation offset is fixed, so the reported line number never changes and no command is small enough to avoid it.
Cause
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");
}
function handleSessionStart(input) {
appendEnvVar(SESSION_ID_ENV, input.session_id);
appendEnvVar(TRANSCRIPT_PATH_ENV, input.transcript_path);
appendEnvVar(PLUGIN_DATA_ENV, process.env[PLUGIN_DATA_ENV]);
}
hooks/hooks.json registers SessionStart with no matcher, so it runs for startup, resume, clear and compact alike. session_id, transcript_path and CLAUDE_PLUGIN_DATA are all constant for the life of a session, so every fire after the first writes bytes that were already there.
Reproduction
Resume or compact a session ~28 times (≈286 bytes per fire on this setup; the threshold is bytes, not fires, so longer paths get there sooner). Then any Bash tool call fails as above.
Observed on this machine — ~/.claude/session-env/<session-id>/sessionstart-hook-3.sh:
201 lines, 3 unique, 19162 bytes # 67 SessionStart fires, broken
90 lines, 3 unique, 8490 bytes # broken
87 lines, 3 unique, 8555 bytes # broken
45 lines, 3 unique, 4440 bytes # still under the limit, works
3 lines, 3 unique, 286 bytes # fresh session, works
Eleven of the session-env files on disk had duplicates; the three over ~8 KB were the ones with dead Bash.
Why it is hard to diagnose
The error names bash and a line number in a string the user never wrote, so it reads as a broken shell profile or a broken PreToolUse hook. It is also session-scoped and correlates with session age, so it looks project-specific or intermittent. Nothing in the plugin appears in the error.
Suggested fix
Make appendEnvVar 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 {
// No env file yet (or unreadable) — fall through and write it.
}
fs.appendFileSync(process.env.CLAUDE_ENV_FILE, line, "utf8");
}
Verified locally against the real hook with a real CLAUDE_ENV_FILE: five identical SessionStart fires produce 3 lines instead of 15; a subsequent fire with a different session_id correctly appends the two changed exports and skips the unchanged CLAUDE_PLUGIN_DATA; the resulting file sources cleanly under set -e.
Alternatives, if you prefer: rewrite the file rather than append (the values are per-session constants, so there is nothing to accumulate), or gate the hook on matcher: "startup" — though that would drop the exports for resumed sessions.
Workaround for anyone hitting this now
Deduplicate the file; content is preserved, since every line is a duplicate of one of three:
f=~/.claude/session-env/<session-id>/sessionstart-hook-3.sh
awk '!seen[$0]++' "$f" > "$f.tmp" && mv "$f.tmp" "$f"
Takes effect on the next Bash call — no restart needed.
SessionStart hook appends duplicate exports to
CLAUDE_ENV_FILEuntil every Bash call failsPlugin:
codex1.0.5 (marketplaceopenai-codex, commit80c31f9)Platform: Windows 10 Pro 19045, Claude Code, Git Bash (
/usr/bin/bash)Summary
handleSessionStartappends threeexportlines to$CLAUDE_ENV_FILEunconditionally.SessionStartfires again on every resume, clear, and compact, so a long-lived session accumulates the same three lines over and over. Claude Code inlines that file into thebash -cprelude for every Bash tool call, and the prelude is truncated past roughly 8 KB. The cut lands mid-string inside a quoted export, so from that point on every Bash invocation in that session fails:echo okincluded. The failure is total and permanent for the affected session — the truncation offset is fixed, so the reported line number never changes and no command is small enough to avoid it.Cause
scripts/session-lifecycle-hook.mjs:hooks/hooks.jsonregistersSessionStartwith no matcher, so it runs forstartup,resume,clearandcompactalike.session_id,transcript_pathandCLAUDE_PLUGIN_DATAare all constant for the life of a session, so every fire after the first writes bytes that were already there.Reproduction
Resume or compact a session ~28 times (≈286 bytes per fire on this setup; the threshold is bytes, not fires, so longer paths get there sooner). Then any Bash tool call fails as above.
Observed on this machine —
~/.claude/session-env/<session-id>/sessionstart-hook-3.sh:Eleven of the session-env files on disk had duplicates; the three over ~8 KB were the ones with dead Bash.
Why it is hard to diagnose
The error names bash and a line number in a string the user never wrote, so it reads as a broken shell profile or a broken PreToolUse hook. It is also session-scoped and correlates with session age, so it looks project-specific or intermittent. Nothing in the plugin appears in the error.
Suggested fix
Make
appendEnvVaridempotent:Verified locally against the real hook with a real
CLAUDE_ENV_FILE: five identicalSessionStartfires produce 3 lines instead of 15; a subsequent fire with a differentsession_idcorrectly appends the two changed exports and skips the unchangedCLAUDE_PLUGIN_DATA; the resulting file sources cleanly underset -e.Alternatives, if you prefer: rewrite the file rather than append (the values are per-session constants, so there is nothing to accumulate), or gate the hook on
matcher: "startup"— though that would drop the exports for resumed sessions.Workaround for anyone hitting this now
Deduplicate the file; content is preserved, since every line is a duplicate of one of three:
Takes effect on the next Bash call — no restart needed.