openai / openai/codex-plugin-cc

SessionStart hook leaks per-plugin CLAUDE_PLUGIN_DATA into the shared session env file

Open
#562 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
JavaScript
Stars
33.3k
Forks
2.3k
PR merge metrics
No merged PRs in 30d

Description

Summary

The SessionStart hook writes this plugin's own CLAUDE_PLUGIN_DATA into $CLAUDE_ENV_FILE, which is shared by every plugin in the session. Because that file is sourced by all subsequent Bash tool calls, the last SessionStart hook to run wins the variable for the whole session — for every consumer, not just its owner.

When two plugins that both do this are installed (e.g. this one and another Claude Code companion), background jobs launched from Bash land in the other engine's state directory. At SessionEnd, that other engine's cleanupSessionJobs finds them (same session_id), calls terminateProcessTree() and prunes them from its state — so the job dies with no error surfaced, and a later status/result call answers No job found for "<id>".

Where

plugins/<name>/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");
}
...
appendEnvVar(PLUGIN_DATA_ENV, process.env[PLUGIN_DATA_ENV]);   // <-- leaks to every plugin

SESSION_ID_ENV and TRANSCRIPT_PATH_ENV are namespaced (<PLUGIN>_COMPANION_SESSION_ID), so they do not collide. CLAUDE_PLUGIN_DATA is the only generic name being exported into the shared file.

Why it matters

Claude Code already provides the correct per-plugin value to each hook invocation. The leak only affects Bash-launched child processes, which is precisely where the companion CLI runs. Consequences observed:

  • Jobs are written to a foreign state directory (resolveStateDir() derives it from $CLAUDE_PLUGIN_DATA).
  • The foreign plugin's SessionEnd terminates and prunes them, since both filter on job.sessionId === sessionId.
  • The failure is silent and non-deterministic — it only bites if some session ends while a long-running job is still polling, which makes it hard to attribute. In a setup with several concurrent sessions this happens regularly.
  • Symmetric: whichever plugin wins the variable, the other one's jobs are the casualties.

Reproduction

  1. Install two Claude Code plugins that both export CLAUDE_PLUGIN_DATA from their SessionStart hook.
  2. In a session, from a Bash tool call: echo "$CLAUDE_PLUGIN_DATA" → it points at whichever plugin's hook ran last, not necessarily the one you are about to invoke.
  3. Launch a background job through this plugin's companion from Bash, then read its logFile via status --json → the path is under the other plugin's data directory.
  4. End another session in the same workspace while the job is queued/running → the job is terminated and pruned; result <jobId> then answers No job found.

Step 3 alone is enough to confirm the leak; step 4 reproduces the resulting job loss.

Notes for whoever picks this up

Two obvious fixes both have a catch, so I am reporting the defect rather than proposing a patch:

  • Simply dropping the appendEnvVar(PLUGIN_DATA_ENV, …) line changes behaviour: with the variable unset, resolveStateDir() falls back to FALLBACK_STATE_ROOT_DIR (os.tmpdir()/<plugin>), so jobs move to the temp directory instead of the plugin data directory.
  • Renaming to a namespaced variable (e.g. <PLUGIN>_PLUGIN_DATA) is not purely local: at least one other published plugin reads the generic CLAUDE_PLUGIN_DATA and throws when it is missing, so a rename by the writers without a matching fallback in the readers would break it.

A direction that avoids both traps is to pass the value explicitly to the child process at spawn time (env of the spawned command) instead of publishing it into the shared session env file — the value is already known to the plugin at that point.

Happy to test a patch against a multi-plugin setup if that helps.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in plugins//scripts/session-lifecycle-hook.mjs, focusing on appendEnvVar, the SessionStart hook, and resolveStateDir. Trace how the companion launches Bash child processes and preserve the plugin-specific data directory without exporting the generic CLAUDE_PLUGIN_DATA into the shared session file. Reproduce with two plugins and confirm jobs remain in the owning plugin's state directory and survive another session's cleanup.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript
Domain
cli, tooling
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.