Skip to content

SessionEnd kills whatever pid broker.json names, without checking it is still a broker (pid reuse → SIGTERM to an unrelated process group) #743

Open

Description

@demisx

Version: 1.0.6 · OS: macOS

scripts/session-lifecycle-hook.mjs handleSessionEnd 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.

Activity

  1. CBEPX commented on Sep 27, 2026

    @CBEPX

    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.

  2. CBEPX commented on Sep 29, 2026

    @CBEPX

    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.

    /plugin marketplace add CBEPX/codex-plugin-cc
    /plugin install codex@cbepx
    

    This is a fork release, not an upstream fix, so I'm leaving this issue open for the maintainers here.

  3. tatsumiyama commented on Oct 15, 2026

    @tatsumiyama

    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.

  4. gealvera commented on Nov 3, 2026

    @gealvera

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions