openai / openai/codex-plugin-cc

SessionEnd never reclaims the app-server broker when the Claude session cwd is not a git repository

Open
#767 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

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:72resolveBrokerStateFile(cwd)path.join(resolveStateDir(cwd), "broker.json")
  • lib/state.mjs:29resolveStateDir(cwd)resolveWorkspaceRoot(cwd)
  • lib/workspace.mjs:3resolveWorkspaceRoot(cwd)ensureGitRepository(cwd), falling back to cwd when it is not a git repo

But SessionEnd looks it up with the Claude session's cwd:

  • session-lifecycle-hook.mjs:82const cwd = input.cwd || process.cwd();
  • session-lifecycle-hook.mjs:84loadBrokerSession(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
  1. 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).
  2. Start a Claude Code session there.
  3. Invoke codex so that it runs against ~/workspace/project.
  4. Confirm the registration landed under the project: ls ~/.claude/plugins/data/codex-openai-codex/state/project-*/broker.json
  5. End the Claude session.
  6. pgrep -f app-server-broker.mjs — the broker is still alive.
  7. 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
  1. 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.
  2. Alternatively, on SessionEnd sweep every broker.json under the plugin data state root and shut down those owned by the ending session.
  3. Pass killProcess: terminateProcessTree at lib/app-server.mjs:340 so the stale-broker path in ensureBrokerSession can 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_ID and CLAUDE_PID (verified with ps -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:29resolveWorkspaceRoot, and ensureBrokerSession reuses a reachable endpoint at lib/broker-lifecycle.mjs:113-116). So a fix must not shut down a broker another live session is still using: check state.json for running/queued jobs whose pid is still alive before terminating. Note that status stays running for 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

  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 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.