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
{{ message }}
Repository navigation
SessionEnd kills whatever pid broker.json names, without checking it is still a broker (pid reuse → SIGTERM to an unrelated process group) #743
scripts/session-lifecycle-hook.mjshandleSessionEnd loads broker.json for the session cwd and passes its pid to teardownBrokerSession, which calls terminateProcessTree(pid) (scripts/lib/broker-lifecycle.mjs, scripts/lib/process.mjs) with no check that the process is still an app-server-broker.mjs — no argv, start-time, or socket-ownership comparison. broker.json is only removed by a SessionEnd whose cwd matches, so entries whose broker died some other way (worktree removed, session killed, machine slept) stay behind indefinitely, and their pids get recycled.
Observed on 1.0.6, macOS, 16 days of uptime: 320 broker.json files under ~/.claude/plugins/data/codex-openai-codex/state/ (343 state dirs in total); 4 of them name a pid that is alive today and belongs to something else:
224-scope-backlog-skills-to-backlog-column-358be27031764eea/broker.json -> pid 73768 is now: Slack ShipIt
265-add-verification-must-be-able-to-fail-rule-c33891ccc7c234eb/broker.json -> pid 21722 is now: login
360-share-master-board-workflow-2cba6deeb0cafbd8/broker.json -> pid 60269 is now: Wispr Flow Helper
97-simplify-push-guard-refspec-config-782d049f8463288a/broker.json -> pid 2046 is now: WallpaperSonomaExtension
A session that ends in a cwd with one of those entries sends SIGTERM to that process group.
Related, already on file (linked rather than repeated): #380 (cwd-hash lookup misses brokers whose job cwd ≠ session cwd, which is how these entries go stale), #605 / #543 (brokers never self-terminate). #521 reports the same missing validation as an attacker-gated security issue on shared Linux hosts with a world-writable os.tmpdir(); this report is the accidental path to the same primitive — no attacker, no shared host, and it reproduces on macOS, which #521 explicitly excludes.
This report is only the identity check: before signalling broker.json's pid, confirm the process is still a broker — e.g. compare its argv against the recorded endpoint/pid-file, or the pid-file's own contents — and drop the entry instead of killing when it is not.
Repro: pick any stale broker.json whose pid is alive and is not a broker, run a session in that cwd, /quit; the unrelated process receives SIGTERM.
This is addressed in the CBEPX fork release v1.3.0; see the changelog for the full list of changes. broker.json (and job records/pid sidecars generally) now carry a pidIdentity instead of a bare pid, so a kill target can be verified before it's signalled. When SessionEnd can't confirm or deliver a kill, it now leaves that job's record in place and logs [codex] SessionEnd left <id> running: <reason>, instead of blindly sending SIGTERM to whatever process now holds the recorded pid. This identity check is posix-only for now — on Windows, kills issued from stored process records stay refused until process identity lands in v1.4.0.
claude plugin marketplace add CBEPX/codex-plugin-cc && claude plugin install codex@cbepx
This is a fork release, not an upstream fix, so I'm leaving this issue open for the maintainers here.
Fixed on Windows in CBEPX/codex-plugin-cc v1.4.1 (https://github.com/CBEPX/codex-plugin-cc/releases/tag/v1.4.1); the posix half was fixed in v1.3.0. SessionEnd and /codex:cancel now verify the recorded pid by its start time before killing, and a record without a captured identity refuses the kill (identity-unavailable) instead of signalling by pid. A verified tree is terminated through the in-box Windows PowerShell 5.1. If the kill cannot be verified, SessionEnd keeps the record (kept=true) and retries at the next SessionEnd.
A related case: the same unchecked-pid problem exists for pids stored in job records (state.json and the jobs files), not only broker.json.
Jobs record only a pid (codex-companion.mjs line 635, lib/tracked-jobs.mjs line 148). Both /codex:cancel (codex-companion.mjs line 923) and the SessionEnd cleanup (session-lifecycle-hook.mjs lines 58-67) run taskkill /PID (pid) /T /F with no identity check.
On Windows 11 with plugin 1.0.1 we found a leftover running job record whose pid had been reused by a cmd.exe of the Codex desktop app. We corrected the records by hand before anything was killed.
Storing the start time with the pid, and checking it before taskkill, would cover both broker.json and job records. A failed cancel on a non-English system (see #423) also leaves such records behind, which makes this more likely.
Confirming this on Windows 11 with 1.0.6 (db52e28), with a concrete near-miss.
On a machine with ~6 days of uptime I found 20 stale broker.json files under ~/.claude/plugins/data/codex-openai-codex/state/ (oldest from two weeks earlier) and 26
leftover %TEMP%\cxc-* session dirs — SessionEnd cleanup never completed for any of them
(consistent with #626 and #525 on Windows). All but one stored PID were free. The remaining one
was not: the PID saved in one project's broker.json had been reused by a claude.exe --type=gpu-process (Claude Desktop) process. The next SessionEnd in that
project would have run taskkill /PID <pid> /T /F against it and its whole tree.
So on Windows the failures compound: cleanup aborts (#626/#525), which leaves broker.json
behind, which turns into a PID-reuse kill later (#743). Checking the process identity before
signalling (start time + command line containing app-server-broker.mjs and the endpoint, as
in #749) would make the stale state harmless even while the other cleanup bugs remain.
Workaround for other Windows users, with all Claude Code sessions closed: Remove-Item "$HOME\.claude\plugins\data\codex-openai-codex\state\*\broker.json".
Activity
CBEPX commentedon Sep 27, 2026
This is addressed in the CBEPX fork release v1.3.0; see the changelog for the full list of changes.
broker.json(and job records/pid sidecars generally) now carry apidIdentityinstead of a bare pid, so a kill target can be verified before it's signalled. WhenSessionEndcan't confirm or deliver a kill, it now leaves that job's record in place and logs[codex] SessionEnd left <id> running: <reason>, instead of blindly sending SIGTERM to whatever process now holds the recorded pid. This identity check is posix-only for now — on Windows, kills issued from stored process records stay refused until process identity lands in v1.4.0.This is a fork release, not an upstream fix, so I'm leaving this issue open for the maintainers here.
docs(triage): v1.4.0 upstream comment drafts and status updates
CBEPX commentedon Sep 29, 2026
Fixed on Windows in CBEPX/codex-plugin-cc v1.4.1 (https://github.com/CBEPX/codex-plugin-cc/releases/tag/v1.4.1); the posix half was fixed in v1.3.0.
SessionEndand/codex:cancelnow verify the recorded pid by its start time before killing, and a record without a captured identity refuses the kill (identity-unavailable) instead of signalling by pid. A verified tree is terminated through the in-box Windows PowerShell 5.1. If the kill cannot be verified,SessionEndkeeps the record (kept=true) and retries at the nextSessionEnd.This is a fork release, not an upstream fix, so I'm leaving this issue open for the maintainers here.
tatsumiyama commentedon Oct 15, 2026
A related case: the same unchecked-pid problem exists for pids stored in job records (state.json and the jobs files), not only broker.json.
Jobs record only a pid (codex-companion.mjs line 635, lib/tracked-jobs.mjs line 148). Both /codex:cancel (codex-companion.mjs line 923) and the SessionEnd cleanup (session-lifecycle-hook.mjs lines 58-67) run taskkill /PID (pid) /T /F with no identity check.
On Windows 11 with plugin 1.0.1 we found a leftover running job record whose pid had been reused by a cmd.exe of the Codex desktop app. We corrected the records by hand before anything was killed.
Storing the start time with the pid, and checking it before taskkill, would cover both broker.json and job records. A failed cancel on a non-English system (see #423) also leaves such records behind, which makes this more likely.
gealvera commentedon Nov 3, 2026
Confirming this on Windows 11 with 1.0.6 (
db52e28), with a concrete near-miss.On a machine with ~6 days of uptime I found 20 stale
broker.jsonfiles under~/.claude/plugins/data/codex-openai-codex/state/(oldest from two weeks earlier) and 26leftover
%TEMP%\cxc-*session dirs — SessionEnd cleanup never completed for any of them(consistent with #626 and #525 on Windows). All but one stored PID were free. The remaining one
was not: the PID saved in one project's
broker.jsonhad been reused by aclaude.exe --type=gpu-process(Claude Desktop) process. The nextSessionEndin thatproject would have run
taskkill /PID <pid> /T /Fagainst it and its whole tree.So on Windows the failures compound: cleanup aborts (#626/#525), which leaves
broker.jsonbehind, which turns into a PID-reuse kill later (#743). Checking the process identity before
signalling (start time + command line containing
app-server-broker.mjsand the endpoint, asin #749) would make the stale state harmless even while the other cleanup bugs remain.
Workaround for other Windows users, with all Claude Code sessions closed:
Remove-Item "$HOME\.claude\plugins\data\codex-openai-codex\state\*\broker.json".