openai / openai/codex-plugin-cc
SessionEnd never reclaims the app-server broker when the Claude session cwd is not a git repository
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 33.3k
- Forks
- 2.3k
- PR merge metrics
- No merged PRs in 30d
Description
Environment
- codex plugin 1.0.4 (Claude Code plugin)
- macOS 26 (Darwin 25.6.0), Node 25.6.1
Summary
app-server-broker.mjs processes accumulate indefinitely. On my machine 11 brokers had been running for up to 10 days, holding ~1.2 GB RSS together with 30 child playwright-mcp processes. They are never reclaimed because the SessionEnd hook looks for broker.json under a different directory than the one it was written to.
Root cause
The broker session file is written under the git repository root of the cwd that codex ran in:
lib/broker-lifecycle.mjs:72—resolveBrokerStateFile(cwd)→path.join(resolveStateDir(cwd), "broker.json")lib/state.mjs:29—resolveStateDir(cwd)→resolveWorkspaceRoot(cwd)lib/workspace.mjs:3—resolveWorkspaceRoot(cwd)→ensureGitRepository(cwd), falling back tocwdwhen it is not a git repo
But SessionEnd looks it up with the Claude session's cwd:
session-lifecycle-hook.mjs:82—const cwd = input.cwd || process.cwd();session-lifecycle-hook.mjs:84—loadBrokerSession(cwd)
When the Claude session runs from a parent workspace directory that is not itself a git repository, while codex runs against git repositories nested inside it, the two paths never agree. loadBrokerSession returns null, sendBrokerShutdown is skipped, and teardownBrokerSession is called with pid: null, so nothing is killed. Since the broker is spawned detached: true + unref() (lib/broker-lifecycle.mjs:64-67), it is reparented to launchd and survives forever.
The BROKER_ENDPOINT_ENV fallback at session-lifecycle-hook.mjs:85-91 does not help: handleSessionStart only exports CODEX_COMPANION_SESSION_ID and CLAUDE_PLUGIN_DATA, so the hook process never sees a broker endpoint.
Verification
// plugin data env set, otherwise unmodified 1.0.4
loadBrokerSession("/Users/me/workspace") // → null (Claude session cwd, not a git repo)
loadBrokerSession("/Users/me/workspace/project") // → { pid: 26667, ... } (where codex actually ran)
Observed state before cleanup: 11 live brokers, each registered under a different project's state dir; 15 broker.json files total; the oldest broker had been running 10 days.
Reproduction
- Use a parent directory that is not a git repository as the Claude Code working directory (e.g.
~/workspace), containing git repositories (e.g.~/workspace/project). - Start a Claude Code session there.
- Invoke codex so that it runs against
~/workspace/project. - Confirm the registration landed under the project:
ls ~/.claude/plugins/data/codex-openai-codex/state/project-*/broker.json - End the Claude session.
pgrep -f app-server-broker.mjs— the broker is still alive.- Repeat; brokers accumulate one per project, indefinitely.
Secondary issue (code inspection only, not reproduced)
lib/app-server.mjs:340 calls ensureBrokerSession(cwd, { env: options.env }) without killProcess. If a recorded broker is found but its endpoint is unreachable, ensureBrokerSession tears it down at lib/broker-lifecycle.mjs:119-129 — but teardownBrokerSession only kills when a killProcess callback is supplied (:174). The stale process is therefore left running while broker.json is overwritten with the new broker, making the old one unreachable for any later cleanup.
Suggested fixes
- Key broker registration by owning session id, or keep a registry the SessionEnd hook can read regardless of cwd, so shutdown does not depend on cwd agreeing.
- Alternatively, on SessionEnd sweep every
broker.jsonunder the plugin data state root and shut down those owned by the ending session. - Pass
killProcess: terminateProcessTreeatlib/app-server.mjs:340so the stale-broker path inensureBrokerSessioncan actually terminate it.
Two things that may help whoever picks this up:
- The owning session is already recorded in the broker process environment. A live broker carries
CODEX_COMPANION_SESSION_IDandCLAUDE_PID(verified withps -wwwE -o command= <pid>), so a cwd-independent sweep can establish ownership without adding any new bookkeeping — including for brokers that were orphaned by earlier versions. - Brokers are shared per workspace, not per session (
lib/state.mjs:29→resolveWorkspaceRoot, andensureBrokerSessionreuses a reachable endpoint atlib/broker-lifecycle.mjs:113-116). So a fix must not shut down a broker another live session is still using: checkstate.jsonforrunning/queuedjobs whose pid is still alive before terminating. Note thatstatusstaysrunningfor long-dead jobs, so the status field alone is not sufficient.
Happy to test a patch.
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 session-lifecycle-hook.mjs, lib/broker-lifecycle.mjs, lib/state.mjs, and lib/workspace.mjs to trace how broker ownership and paths are recorded and resolved. Reproduce the parent-workspace scenario, then inspect state.json and broker.json handling for shared workspaces and live jobs. Done means SessionEnd reliably reclaims only brokers owned by the ending session, including unreachable stale brokers, without stopping one used by another live session.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, node.js
- Domain
- devtools
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 56/100