openai / openai/codex-plugin-cc
SessionEnd cannot find the broker when CLAUDE_PLUGIN_DATA differs between spawn and teardown — same cwd, same hash, different state root
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 33.3k
- Forks
- 2.3k
- PR merge metrics
- No merged PRs in 30d
Description
Summary
resolveStateDir() picks the state root from CLAUDE_PLUGIN_DATA, falling back to
$TMPDIR/codex-companion when that variable is absent. The workspace slug and hash are
identical in both cases — only the root differs. When a broker is registered while
the variable is unset and SessionEnd later runs with it set (or vice versa),
loadBrokerSession(cwd) reads the other root, finds nothing, and the broker is
orphaned permanently.
This is not #380. There, a different cwd produces a different hash. Here the cwd and
the hash are identical — wienerdog-cc76dfe898ef0cd4 exists in both roots on my
machine — and the lookup still misses.
Mechanism
scripts/lib/state.mjs:
const FALLBACK_STATE_ROOT_DIR = path.join(os.tmpdir(), "codex-companion");
// ...
const pluginDataDir = process.env[PLUGIN_DATA_ENV]; // CLAUDE_PLUGIN_DATA
const stateRoot = pluginDataDir ? path.join(pluginDataDir, "state") : FALLBACK_STATE_ROOT_DIR;
return path.join(stateRoot, `${slug}-${hash}`);
Same cwd, two answers:
$ CLAUDE_PLUGIN_DATA=~/.claude/plugins/data/codex-openai-codex \
node -e 'import("…/lib/state.mjs").then(m=>console.log(m.resolveStateDir("…/wienerdog")))'
/Users/…/.claude/plugins/data/codex-openai-codex/state/wienerdog-cc76dfe898ef0cd4
$ env -u CLAUDE_PLUGIN_DATA \
node -e 'import("…/lib/state.mjs").then(m=>console.log(m.resolveStateDir("…/wienerdog")))'
/var/folders/…/T/codex-companion/wienerdog-cc76dfe898ef0cd4
There is no second chance. handleSessionEnd falls back to the endpoint env var:
const brokerSession =
loadBrokerSession(cwd) ??
(process.env[BROKER_ENDPOINT_ENV] ? { endpoint: process.env[BROKER_ENDPOINT_ENV], … } : null);
but CODEX_COMPANION_APP_SERVER_ENDPOINT is never exported to the hook environment —
handleSessionStart propagates only CODEX_COMPANION_SESSION_ID,
CODEX_COMPANION_TRANSCRIPT_PATH and CLAUDE_PLUGIN_DATA via appendEnvVar. So once
the root lookup misses, endpoint is null, no broker/shutdown is sent, and
teardownBrokerSession gets nothing to kill.
Evidence from a real machine
One workspace, split across both roots:
A) $TMPDIR/codex-companion/wienerdog-cc76dfe898ef0cd4/
broker.json Jul 19 02:12 <- pid 68646
jobs/ Jul 19 19:58 (64 entries)
state.json Jul 19 20:01 (32 KB)
B) ~/.claude/plugins/data/codex-openai-codex/state/wienerdog-cc76dfe898ef0cd4/
jobs/ Aug 10 00:35 (empty)
state.json Aug 10 00:35 (80 bytes)
(no broker.json)
broker.json in root A still records "pid": 68646, and that process is alive after
23 days, with zero clients connected (verified via lsof on its unix socket: only the
broker itself holds the listening fd).
The 64 job entries and the 32 KB state.json in root A show this was not a
first-run artifact — real work ran against the fallback root for ~18 hours.
Inferred sequence (consistent with all timestamps, not directly observed): the
workspace was revisited on Aug 10 with CLAUDE_PLUGIN_DATA set. loadBrokerSession(cwd)
resolved to root B, found nothing, so existing was null — ensureBrokerSession
skipped the teardown branch entirely and spawned a second broker. That one was reaped
normally at SessionEnd (no broker.json remains in root B), while pid 68646 in root A
was never referenced again by any code path.
So the root split does not merely orphan a broker — it also causes a duplicate broker to
be spawned for the same workspace, since ensureBrokerSession cannot see the existing one.
Why the existing issues don't cover this
- #380 — cwd mismatch → different hash. Here the hash matches exactly; the root differs.
- #543 / #108 — broker never self-terminates / no idle timeout. Correct and complementary:
an idle timer would eventually reap this orphan, but the lookup defect would still cause
duplicate brokers and split job state on every affected session. - #288 —
sendBrokerShutdownhas no timeout. Different failure point; here the shutdown
is never attempted at all becauseendpointisnull.
Suggested fixes
- Make the state root deterministic. Resolve it once and record the chosen root inside
broker.json, or drop theCLAUDE_PLUGIN_DATAconditional and always use a fixed
location. A root that depends on ambient environment cannot be relied on across a
process boundary. - Fall back across roots on lookup.
loadBrokerSession(cwd)could check the alternate
root when the primary misses. Cheap, and fixes existing orphans on the next session. - Export the broker endpoint to the hook environment so the
BROKER_ENDPOINT_ENV
fallback inhandleSessionEndis actually reachable. Today it is dead code for hooks. - Independently, the idle self-exit proposed in #543 would bound the damage from this and
every other lookup-side failure.
Environment
- macOS (darwin 25.5.0), Node v24.18.0
- codex plugin 1.0.6 (
openai/codex-plugin-cc) - 9 orphaned broker chains total on this machine, ~5.1 GB RSS, 0 clients on all 9
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with scripts/lib/state.mjs, especially resolveStateDir() and loadBrokerSession(cwd), then trace handleSessionStart, handleSessionEnd, and appendEnvVar to understand how state roots and broker endpoints move between hooks. Done means a broker registered under either environment setup is found and shut down at SessionEnd without creating a duplicate broker.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, node.js
- Domain
- backend, tooling
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100