You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The plugin runs one long-lived codex app-serverper workspace, but spawns it with the inherited environment and never sets CODEX_HOME. When two Claude Code sessions are active in different workspaces (separate repos, or separate git worktrees of one repo), two app-servers run concurrently against the same~/.codex and race on codex's process-global mutable state. The result is that the two codex "sessions" interfere and the earlier review fails.
Environment
Plugin codex@openai-codexv1.0.4
Codex CLI 0.139.0
macOS (darwin), Node v26
Root cause
The app-server is spawned inheriting ambient env, with no CODEX_HOME: plugins/codex/scripts/lib/app-server.mjs → spawn("codex", ["app-server"], { env: this.options.env ?? process.env }) (~L188-189).
It is launched via the broker, also with env = process.env: plugins/codex/scripts/lib/broker-lifecycle.mjs → spawnBrokerProcess({ … env = process.env }) (L59), called from ensureBrokerSession (L113).
The broker — and therefore the app-server — is keyed per workspace, not per session: plugins/codex/scripts/lib/state.mjs → resolveStateDir(cwd) = <CLAUDE_PLUGIN_DATA>/state/<slug>-<sha256(workspaceRoot)> (L29-43).
⇒ N workspaces ⇒ N concurrent app-servers, all sharing one ~/.codex. That directory is not safe for concurrent multi-process writers: .codex-global-state.json is rewritten via a temp-file + rename pattern, and codex continuously writes a set of SQLite stores and index files there.
Evidence
On a heavily-used machine, ~/.codex accumulates orphaned, half-written atomic-rename temp files — the fingerprint of concurrent writers clobbering each other:
Seed it with a strict allowlist so login/config are preserved while all runtime state is isolated (default = isolate; only bring across vetted, read-mostly assets, so future additions can never be accidentally shared):
auth.json → symlink to the user's real $CODEX_HOME/auth.json (single source of truth; safe across OAuth refresh-token rotation).
Static user assets (AGENTS.md, rules/, policy/, *.config.toml profiles) → symlink.
Everything else (sessions/, .codex-global-state.json*, *.sqlite*, session_index.jsonl, skills/, models_cache.json, logs, caches, …) → let codex recreate inside the isolated home. This is the state that must not be shared.
Why per-workspace (not per-session): the broker is already 1-per-workspace and serializes concurrent requests (it returns a BUSY code; cf. #342). A per-workspace home yields exactly one writer per home, matching the broker. Same-workspace sessions still share correctly; different workspaces become fully isolated. The plugin already injects CODEX_COMPANION_SESSION_ID at SessionStart (session-lifecycle-hook.mjs L77), so the lifecycle plumbing is largely in place; tie the home's lifecycle to the broker and remove it in teardownBrokerSession.
Verified workaround (until fixed)
A user-level Claude Code SessionStart hook that allocates a per-session CODEX_HOME (same allowlist seeding as above) and exports it via $CLAUDE_ENV_FILE; the spawned app-server inherits it. Confirmed end-to-end: CODEX_HOME=<seeded home> codex login status → Logged in using ChatGPT, while all *.sqlite / session_index.jsonl / skills/ / global-state stay isolated. Happy to share the script or open a PR.
Alternatives considered
CLI-level concurrency safety (advisory locks / atomic single-writer / SQLite busy-timeout, or a CODEX_STATE_HOME separate from the config home) — the deeper fix, but in openai/codex, not here; per-workspace isolation side-steps it and is shippable in this repo. Related upstream: Multiple parallel codex exec instances interfere via shared session restore codex#11435, #14233, #10887.
codex exec --ephemeral exists but only for exec; the plugin uses app-server, which has no such flag.
Open questions
Default-on vs an opt-in config flag, since per-workspace isolation stops sharing codex's cross-project memory/history.
Seeding policy (symlink vs copy) and refresh-token handling for auth.json.
Summary
The plugin runs one long-lived
codex app-serverper workspace, but spawns it with the inherited environment and never setsCODEX_HOME. When two Claude Code sessions are active in different workspaces (separate repos, or separate git worktrees of one repo), two app-servers run concurrently against the same~/.codexand race on codex's process-global mutable state. The result is that the two codex "sessions" interfere and the earlier review fails.Environment
codex@openai-codexv1.0.4Root cause
CODEX_HOME:plugins/codex/scripts/lib/app-server.mjs→spawn("codex", ["app-server"], { env: this.options.env ?? process.env })(~L188-189).env = process.env:plugins/codex/scripts/lib/broker-lifecycle.mjs→spawnBrokerProcess({ … env = process.env })(L59), called fromensureBrokerSession(L113).plugins/codex/scripts/lib/state.mjs→resolveStateDir(cwd)=<CLAUDE_PLUGIN_DATA>/state/<slug>-<sha256(workspaceRoot)>(L29-43).~/.codex. That directory is not safe for concurrent multi-process writers:.codex-global-state.jsonis rewritten via a temp-file + rename pattern, and codex continuously writes a set of SQLite stores and index files there.Evidence
On a heavily-used machine,
~/.codexaccumulates orphaned, half-written atomic-rename temp files — the fingerprint of concurrent writers clobbering each other:And the set of files codex mutates at runtime (mtimes captured during a single active session, within a ~2-minute window) is large and shared:
Two app-servers writing all of the above against the same
~/.codexwith no cross-process coordination is the race.Reproduction
/codex:review(or the stop-time review gate) in both near-simultaneously.~/.codexaccumulates..codex-global-state.json.tmp-*orphans.Proposed fix — isolate
CODEX_HOMEper app-server, keyed by the existing workspace identitySet
CODEX_HOMEwhen spawning the broker/app-server, reusing the same per-workspace key already used forbroker.json:Seed it with a strict allowlist so login/config are preserved while all runtime state is isolated (default = isolate; only bring across vetted, read-mostly assets, so future additions can never be accidentally shared):
auth.json→ symlink to the user's real$CODEX_HOME/auth.json(single source of truth; safe across OAuth refresh-token rotation).config.toml,hooks.json→ copy (codex writes back trust hashes / feature toggles / onboarding; copying keeps those writes local), preserving0600.AGENTS.md,rules/,policy/,*.config.tomlprofiles) → symlink.sessions/,.codex-global-state.json*,*.sqlite*,session_index.jsonl,skills/,models_cache.json, logs, caches, …) → let codex recreate inside the isolated home. This is the state that must not be shared.Why per-workspace (not per-session): the broker is already 1-per-workspace and serializes concurrent requests (it returns a BUSY code; cf. #342). A per-workspace home yields exactly one writer per home, matching the broker. Same-workspace sessions still share correctly; different workspaces become fully isolated. The plugin already injects
CODEX_COMPANION_SESSION_IDat SessionStart (session-lifecycle-hook.mjsL77), so the lifecycle plumbing is largely in place; tie the home's lifecycle to the broker and remove it inteardownBrokerSession.Verified workaround (until fixed)
A user-level Claude Code
SessionStarthook that allocates a per-sessionCODEX_HOME(same allowlist seeding as above) and exports it via$CLAUDE_ENV_FILE; the spawned app-server inherits it. Confirmed end-to-end:CODEX_HOME=<seeded home> codex login status→Logged in using ChatGPT, while all*.sqlite/session_index.jsonl/skills// global-state stay isolated. Happy to share the script or open a PR.Alternatives considered
CODEX_STATE_HOMEseparate from the config home) — the deeper fix, but inopenai/codex, not here; per-workspace isolation side-steps it and is shippable in this repo. Related upstream: Multiple parallel codex exec instances interfere via shared session restore codex#11435, #14233, #10887.codex exec --ephemeralexists but only forexec; the plugin usesapp-server, which has no such flag.Open questions
auth.json.Related
#380, #377, #342, #367