@minhspark/codex-mcp-bridge 1.13.9 → 1.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,7 +2,40 @@
2
2
 
3
3
  Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [SemVer](https://semver.org/).
4
4
 
5
- ## [Unreleased]
5
+ ## [1.15.0] - 2026-09-07
6
+
7
+ ### Added
8
+
9
+ - Keep Claude and Codex MCP stdio connections open through a stable supervisor, with immutable worker snapshots and automatic activation of compatible installed-source changes when safely idle. Packaged bridge commands and all three MCP installers use the supervisor.
10
+ - Expose automatic reload state and pending-update blockers in status diagnostics. Failed candidates retain the working generation; active calls, pending deliveries, late replies, and native relay work defer replacement without replaying prompts.
11
+
12
+ ### Fixed
13
+
14
+ - Preserve existing Claude MCP entry properties and native companion environment settings during installer upgrades. Codex registrations refuse to overwrite custom access, timeout, or transport settings and print the exact launcher fields to change in place. Accept or preserve an explicit Claude data directory without binding an account.
15
+
16
+ ### Upgrade notes
17
+
18
+ - Re-run the relevant installers and reconnect each MCP server once in its existing task to load the supervisor. A previously running direct-entry process cannot adopt this mechanism from a disk update alone. Subsequent compatible worker updates do not require an application restart.
19
+ - Automatic reload observes installed files; npm/Git updates remain the responsibility of the existing update mechanism. Client environment changes and incompatible supervisor changes still require MCP reconnect. Pending or uncertain deliveries can defer an update until safely resolved; never resend them merely to unblock a reload.
20
+
21
+ ## [1.14.0] - 2026-09-07
22
+
23
+ ### Added
24
+
25
+ - Rediscover Claude Desktop and Codex account identity for each Desktop call, including repeated account switches and Windows Store Claude installations. Add `target: "auto"` for the unique current-account Claude Code task in an exact project directory, with native task identity retained across CLI restarts.
26
+ - Bind pending replies and Codex creation receipts to their original account fingerprints. Verify the calling Claude Code task through process ancestry, reject lingering old-account callers, and hide delayed results after an account change without repeating an uncertain delivery.
27
+
28
+ ### Fixed
29
+
30
+ - Clear automatically discovered native relay endpoints after disconnect, error, or close so the next request can discover a replacement while preserving explicit socket overrides and uncertain-delivery semantics.
31
+ - Recheck account identity after asynchronous transport connection and before native dispatch. Account-bound requests use protocol v2 and a separate companion endpoint so an older shared companion cannot accept them without enforcement.
32
+ - Isolate Windows MCP contract fixtures from the machine's live native relay pipe. Skip file-symlink tests only when Windows denies creating the test link.
33
+ - Read Windows caller ancestry through a bounded native process snapshot instead of WMI service queries. Clean up integration fixtures even when initialization fails, and fail creation gates promptly when preflight rejects a request.
34
+
35
+ ### Upgrade notes
36
+
37
+ - Reconnect both Desktop MCP clients once after upgrading. Codex can defer its reload until the next active turn. Verify the loaded version and current runtime from that task; subsequent account changes are inspected on each call.
38
+ - Account detection uses Claude's local sign-in snapshot and Codex file-backed ChatGPT identity. Unsupported or transitional identity states stop dispatch until the next call can verify them. Legacy creation receipts without account fingerprints remain protected from implicit reuse.
6
39
 
7
40
  ## [1.13.9] - 2026-09-07
8
41
 
package/README.md CHANGED
@@ -36,6 +36,16 @@ Desktop mode never contacts, starts, or falls back to an external app-server, in
36
36
 
37
37
  `list_codex_threads` filters Desktop's recent/pinned task snapshot by authorization, workspace/title, and result limit. The snapshot can omit agent-created tasks visible in the sidebar and does not cover the complete archive. Absence from this list does not mean creation failed: read the returned task ID directly. Desktop does not expose the external server's `loadedOnly` state; requesting it returns an explanation without contacting that server.
38
38
 
39
+ ### Switching Desktop accounts
40
+
41
+ Desktop calls reread both applications' stored account identity on every invocation and immediately before dispatch. Switching A → B → A does not require a manual peer binding. For Codex → Claude, use `send_to_claude_session` with `target: "auto"` and the exact `expectedCwd`; it selects the current account's only live Code task in that directory. Supply `expectedTaskId` when several tasks match. An explicit task ID stays pinned to that task; if its CLI process restarts, the bridge can rediscover the same native task without choosing a different one.
42
+
43
+ Claude → Codex verifies the calling Code task through the MCP process's parent ancestry and current-account task metadata. An old-account process that remains alive cannot dispatch under the new login. Replies and creation receipts retain both original account fingerprints. A login change during a pending operation hides the old reply and blocks forwarding to the new account; it never repeats an uncertain send or creates a replacement task. Legacy creation receipts without account identity require inspection of their existing task before reuse.
44
+
45
+ Claude detection reads its Desktop sign-in snapshot, including Windows Store installations. Codex detection currently supports file-backed ChatGPT login identity in `auth.json`; keyring, automatic/ephemeral credential storage, API keys, and external tokens return an unavailable state. These local records establish routing identity, not a fresh server authentication check. During logout or an incomplete sign-in, dispatch stops; the next call inspects the records again. The bridge returns fingerprints and status, never tokens or email addresses.
46
+
47
+ After installing this update, reconnect both MCP clients once. Codex may defer its MCP reload until the next active turn; verify the loaded version and `runtime state: current` from that task. Subsequent account changes are detected without a source reload. Account-bound requests use protocol v2 through a separate `-accounts-v2` companion endpoint, so a previous companion holding the legacy socket does not need to be killed and cannot silently ignore the identity checks. Automatically discovered native relay endpoints are discarded after disconnect so the next request discovers the new endpoint; explicit endpoint overrides remain fixed.
48
+
39
49
  ## Architecture
40
50
 
41
51
  Two MCP servers, one living inside each agent:
@@ -112,7 +122,7 @@ Two ways in. Pick by what you intend to do with it.
112
122
  npm install -g @minhspark/codex-mcp-bridge
113
123
  ```
114
124
 
115
- Already installed? The same command with `@latest` upgrades it in place — then restart the client. Details in [Upgrading an install you already have](#upgrading-an-install-you-already-have):
125
+ Already installed? The same command with `@latest` upgrades it in place. Once the supervisor is registered, compatible updates load automatically without restarting the client. Details in [Upgrading an install you already have](#upgrading-an-install-you-already-have):
116
126
 
117
127
  ```bash
118
128
  npm install -g @minhspark/codex-mcp-bridge@latest
@@ -128,9 +138,15 @@ Either route puts the bridge servers, configuration installers, native relay, an
128
138
 
129
139
  #### Upgrading an install you already have
130
140
 
131
- `npm install -g @minhspark/codex-mcp-bridge@latest`, then restart Claude Desktop or Claude Code — an MCP server only loads its code when the client spawns it. Check `codex_bridge_status` reports the version you expect. The global install path carries no version number, so the entry keeps pointing at the right file. If the existing entry still has `CODEX_BRIDGE_THREAD_POLICY=owned`, pass `CODEX_BRIDGE_THREAD_POLICY=roots` once when re-running the installer to enable human-opened threads.
141
+ Install the new files with `npm install -g @minhspark/codex-mcp-bridge@latest`. To migrate an existing direct-entry installation, run `codex-mcp-bridge-install` for Claude Desktop, `claude-mcp-bridge-install` for Codex, and `codex-native-relay-install --no-bootstrap` if the native companion is installed. Update Claude Code's separate registration as shown below. Reconnect each MCP server once in its existing task so it loads the supervisor. No replacement task or full app exit is required when the client exposes MCP reconnect/reload. A process launched before this upgrade cannot acquire the supervisor merely because files changed on disk.
142
+
143
+ After this one-time migration, the stable supervisor keeps the client's MCP stdio connection open while it starts each worker from an immutable source snapshot. The installers prepare this snapshot before changing the registration, avoiding a slow first copy during MCP initialization. Compatible installed-source changes become candidates after the tree stabilizes. A candidate must initialize successfully before it can replace an idle worker. Active calls, pending deliveries, late replies, and relay work defer the switch; an update never silently resends a prompt. A failed candidate leaves the existing worker and its delivery state available.
144
+
145
+ Call `codex_bridge_status`, `claude_bridge_status`, or `native_relay_status` from the actual app task to inspect `autoReload`, the loaded version, and any pending update or blocker. Reading an old message receipt still refers to the worker that accepted that message. Pending or uncertain messages can keep an update deferred until they are resolved; a new process or an unknown message ID is never evidence that a send failed. If the supervisor protocol itself changes incompatibly, reconnect that MCP server once again.
146
+
147
+ Auto-reload watches the **installed source at the configured path**. It does not poll npm, download a release, run `git pull`, or upgrade Claude/Codex themselves. Keep the normal package update mechanism to install new versions. Changes to environment variables or access settings supplied by the MCP client require a client MCP reload to pass the new configuration to the supervisor. Subsequent account switches remain detectable per call without this configuration reload.
132
148
 
133
- Re-running `codex-mcp-bridge-install` is **not** required to upgrade, and before 1.11.1 it actively hurt: it replaced the whole entry, discarding `CODEX_BRIDGE_ALLOWED_THREADS`, any hand-added `CODEX_BRIDGE_THREAD_POLICY`, and resetting `CODEX_BRIDGE_ALLOWED_ROOTS` to the install directory. From 1.11.1 it keeps what is already there — an environment variable you pass wins, the existing value is the fallback, and `--reset` gives you the defaults back.
149
+ The installers preserve existing environment and access settings; explicit supported environment variables override their stored values. Claude's `--reset` explicitly returns to defaults. Codex entries with custom access, timeout, or transport settings are left intact with the exact command and arguments to update in place. `CLAUDE_DESKTOP_USER_DATA` pins only the data directory, never an account identity. Re-running an installer is unnecessary for later compatible source updates. The global install path remains stable across package versions.
134
150
 
135
151
  #### Show the final npm installation result
136
152
 
@@ -157,7 +173,7 @@ npm install -g @minhspark/codex-mcp-bridge@latest
157
173
  | Dry run | `Dry run completed: ... (no changes applied).` |
158
174
  | Mode or installed metadata cannot be verified | A warning instead of a success claim |
159
175
 
160
- The footer runs after npm finishes and keeps npm's output and exit code. It supports `install`/`i`, explicit global installation, one bridge package, dry-run flags, common install booleans, and a single `--prefix`. Other commands, multiple packages, unknown options, and machine-readable or silent output modes pass through without a footer. Installing the package itself does not alter shell profiles. A successful install confirms files on disk; restart the MCP client separately to load the new bridge code.
176
+ The footer runs after npm finishes and keeps npm's output and exit code. It supports `install`/`i`, explicit global installation, one bridge package, dry-run flags, common install booleans, and a single `--prefix`. Other commands, multiple packages, unknown options, and machine-readable or silent output modes pass through without a footer. Installing the package itself does not alter shell profiles. A successful install confirms files on disk; the app's bridge status separately confirms whether its supervisor has activated that version.
161
177
 
162
178
  **Clone it** — right if you intend to read, test or change the code:
163
179
 
@@ -167,7 +183,7 @@ cd codex-mcp-bridge
167
183
  npm install
168
184
  ```
169
185
 
170
- A clone upgrades with `git pull && npm ci`, then the same restart.
186
+ A clone upgrades with `git pull && npm ci`; a registered supervisor activates a compatible stable update when its worker is safely idle. Direct `node src/index.mjs` and `node src/claude-bridge.mjs` remain available for diagnostics but bypass automatic reload. Use the registered supervisor or packaged bridge commands for long-lived clients.
171
187
 
172
188
  Confirm the tree is healthy before wiring it into anything:
173
189
 
@@ -217,7 +233,7 @@ The result on macOS:
217
233
  "mcpServers": {
218
234
  "codex-bridge": {
219
235
  "command": "/Users/<user>/.local/node/v24.18.0/bin/node",
220
- "args": ["/Users/<user>/code/codex-mcp-bridge/src/index.mjs"],
236
+ "args": ["/Users/<user>/code/codex-mcp-bridge/src/mcp-supervisor.mjs", "index.mjs"],
221
237
  "env": {
222
238
  "CODEX_BIN": "/Users/<user>/.local/bin/codex",
223
239
  "CODEX_APP_SERVER_URL": "ws://127.0.0.1:8791"
@@ -227,19 +243,19 @@ The result on macOS:
227
243
  }
228
244
  ```
229
245
 
230
- Restart Claude Desktop afterwards. To write the same entry by hand, use absolute paths for both `command` and `args` — Claude Desktop does not resolve either from PATH.
246
+ Reconnect `codex-bridge` once in the existing Claude task, then verify `codex_bridge_status` reports auto-reload enabled. To write the same entry by hand, use absolute paths for `command` and the supervisor script; its second argument is the worker filename.
231
247
 
232
248
  ### 3. Claude → Codex, into Claude Code (CLI)
233
249
 
234
250
  Claude Code keeps its own MCP registry, so it needs its own registration. Run this from the repo root:
235
251
 
236
252
  ```bash
237
- claude mcp add codex-bridge --scope user -e CODEX_BIN="$(command -v codex)" -- "$(command -v node)" "$PWD/src/index.mjs"
253
+ claude mcp add codex-bridge --scope user -e CODEX_BIN="$(command -v codex)" -- "$(command -v node)" "$PWD/src/mcp-supervisor.mjs" index.mjs
238
254
  ```
239
255
 
240
256
  ```powershell
241
257
  # Windows (PowerShell 7+)
242
- claude mcp add codex-bridge --scope user -e CODEX_BIN="$((Get-Command codex).Source)" -- "$((Get-Command node).Source)" "$PWD\src\index.mjs"
258
+ claude mcp add codex-bridge --scope user -e CODEX_BIN="$((Get-Command codex).Source)" -- "$((Get-Command node).Source)" "$PWD\src\mcp-supervisor.mjs" index.mjs
243
259
  ```
244
260
 
245
261
  Check and remove it with:
@@ -250,7 +266,7 @@ claude mcp get codex-bridge
250
266
  claude mcp remove codex-bridge --scope user
251
267
  ```
252
268
 
253
- `--scope user` makes the bridge available in every project. Use `--scope project` instead to commit the entry into a repo's `.mcp.json` and share it with the team.
269
+ `--scope user` makes the bridge available in every project. Use `--scope project` instead to commit the entry into a repo's `.mcp.json` and share it with the team. For an existing registration, preserve its environment and access settings while replacing only the launcher command/arguments. Use the current session's `/mcp` reconnect action once, then verify `codex_bridge_status` from that session.
254
270
 
255
271
  ### 4. Codex → Claude, into Codex
256
272
 
@@ -261,7 +277,7 @@ node scripts/install-codex-mcp.mjs
261
277
  The installer updates the named entry through `codex mcp add`, preserves existing environment settings, pins the configured Desktop policy, and disables external autostart in Desktop mode. It refuses to reset custom access, timeout, or transport settings. It never infers the sender permission class from the recipient. The basic registration by hand is:
262
278
 
263
279
  ```bash
264
- codex mcp add claude-bridge --env CLAUDE_BRIDGE_PEER_NAME=codex-desktop -- "$(command -v node)" "$PWD/src/claude-bridge.mjs"
280
+ codex mcp add claude-bridge --env CLAUDE_BRIDGE_PEER_NAME=codex-desktop -- "$(command -v node)" "$PWD/src/mcp-supervisor.mjs" claude-bridge.mjs
265
281
  ```
266
282
 
267
283
  Verify and remove:
@@ -272,7 +288,7 @@ codex mcp get claude-bridge
272
288
  node scripts/install-codex-mcp.mjs --remove
273
289
  ```
274
290
 
275
- Reconnect the MCP server in the existing Codex Desktop task, or restart the app and reopen that same task. Verify `claude_bridge_status` **from that task**: `runtime state` must be `current` and `session policy` must match the intended mode. Do not create a replacement task. `npm run check:claude` starts a separate diagnostic process and cannot prove that the app's existing MCP connection reloaded.
291
+ Reconnect the MCP server once in the existing Codex Desktop task. Verify `claude_bridge_status` **from that task**: `autoReload` must be enabled, `runtime state` must be `current`, and `session policy` must match the intended mode. Codex may apply a requested reload on its next active turn. Do not create a replacement task. `npm run check:claude` starts a separate diagnostic process and cannot prove that the app's existing MCP connection reloaded.
276
292
 
277
293
  `CLAUDE_BRIDGE_PEER_NAME` sets the name Claude shows for this bridge in its agent list.
278
294
 
@@ -284,7 +300,7 @@ Optional, and only worth installing if you keep the bound thread **open in Codex
284
300
  node scripts/install-native-relay.mjs
285
301
  ```
286
302
 
287
- This registers the companion with Codex (`codex mcp add codex-native-relay -- <node> src/native-relay-companion.mjs`) and bootstraps the executor thread the native dispatch needs, writing its id to `~/.codex/native-relay.json`. The `<node>` is resolved rather than inherited: Codex Desktop authenticates the code-signing identity of any process that connects to its native tools pipe, so on macOS the installer registers the runtime the app ships and prints the `relay runtime:` line naming it and where it came from. `CODEX_NATIVE_RELAY_NODE` overrides that on any platform; Windows and Linux keep the Node running the installer, as before. The bootstrap creates one thread, unsubscribes it, and closes its connection. It leaves other clients and threads running; an idle server may retain the executor until its configured unload delay expires.
303
+ This registers the companion with Codex (`codex mcp add codex-native-relay -- <node> src/mcp-supervisor.mjs native-relay-companion.mjs`) and bootstraps the executor thread the native dispatch needs, writing its id to `~/.codex/native-relay.json`. The installer preserves the existing environment and refuses to reset custom access, timeout, or transport settings. The `<node>` is resolved rather than inherited: Codex Desktop authenticates the code-signing identity of any process that connects to its native tools pipe, so on macOS the installer registers the runtime the app ships and prints the `relay runtime:` line naming it and where it came from. `CODEX_NATIVE_RELAY_NODE` overrides that on any platform; Windows and Linux keep the Node running the installer, as before. The supervisor uses that same runtime for its worker. The bootstrap creates one thread, unsubscribes it, and closes its connection. It leaves other clients and threads running; an idle server may retain the executor until its configured unload delay expires.
288
304
 
289
305
  ```bash
290
306
  codex mcp get codex-native-relay
@@ -292,7 +308,7 @@ node scripts/install-native-relay.mjs --no-bootstrap # register only; supply C
292
308
  node scripts/install-native-relay.mjs --remove # unregister, and remove the relay config and thread id
293
309
  ```
294
310
 
295
- Restart Codex Desktop so it launches the companion, then call its `native_relay_status` tool — or `claude_bridge_status`, whose `delivery:` line names the backend in force. Until both the companion and the executor thread are in place, `claude-bridge` keeps using the app-server path exactly as before.
311
+ Reconnect `codex-native-relay` once in the existing Codex Desktop task so it launches the supervisor, then call its `native_relay_status` tool and verify auto-reload is enabled. `claude_bridge_status` also names the active delivery backend. Until both the companion and the executor thread are in place, `claude-bridge` keeps using the app-server path exactly as before.
296
312
 
297
313
  ### 6. macOS only: keep the app-server alive with launchd
298
314
 
@@ -392,13 +408,13 @@ Desktop task mode (`CODEX_BRIDGE_DESKTOP_TASKS=1` or the shared setting written
392
408
 
393
409
  `claude_bridge_status` reports the destination policy, eligible session count, and excluded non-Desktop count. Send receipts include the destination's `entrypoint`, `cwd`, and `sessionId`. A correlated reply confirms receipt in that session; it does not independently prove that its conversation is visible in the Desktop UI. Desktop sends require `expectedCwd`, an absolute independently verified project directory. The bridge resolves both paths and refuses a missing, nonexistent, parent, or different directory before sending; it rechecks the destination after queueing. Reconnect the bridge's MCP connection after upgrading to load the enforcement code.
394
410
 
395
- Desktop session discovery includes the native task title and task ID, matched through Claude Desktop's local session metadata. Use `list_claude_sessions({expectedCwd})` for the intended project; an empty result is not permission to substitute a different project. Sending additionally requires `expectedTaskId` from the intended task, alongside `target` and `expectedCwd`. The bridge verifies the exact CLI session ID, unique native task ID, canonical cwd, archive state, optional bridge ID, and live process start identity. It repeats these checks at dispatch. Missing, stale, ambiguous, or unreadable metadata blocks the send rather than guessing from a generated name such as `pcc4sh-19`. Each matched Desktop row also reports the task's permission mode from that metadata and its Claude parity class (`bypass` for `bypassPermissions`; `prompting` for `default`, `acceptEdits`, `auto`, and `dontAsk`; no class for `plan` or an unknown value).
411
+ Desktop session discovery includes the native task title and task ID, matched through the current Claude Desktop account's local session metadata. Use `list_claude_sessions({expectedCwd})` for the intended project; an empty result is not permission to substitute a different project. Explicit targets require `expectedTaskId` from the intended task, alongside `target` and `expectedCwd`. With `target: "auto"`, `expectedTaskId` is optional only when exactly one task matches the current account and project. The bridge verifies the exact CLI session ID, unique native task ID, canonical cwd, archive state, optional bridge ID, and live process start identity. It repeats these checks at dispatch. Missing, stale, ambiguous, or unreadable metadata blocks the send rather than guessing from a generated name such as `pcc4sh-19`. Each matched Desktop row also reports the task's permission mode from that metadata and its Claude parity class (`bypass` for `bypassPermissions`; `prompting` for `default`, `acceptEdits`, `auto`, and `dontAsk`; no class for `plan` or an unknown value).
396
412
 
397
413
  A `held` receipt confirms only that the recipient has withheld a message. It does not prove an approval button is available in Claude Desktop. Inspect that exact task before directing the user to approve; when the UI offers no approval route, preserve the receipt and report the boundary. A preflight failure is distinct: it returns `sent: false` and no message ID, so nothing was submitted to the recipient.
398
414
 
399
415
  Sends to the same Claude session run in order. An outstanding message blocks all further sends to that session, including `waitSec: 0`; changing wait time must not bypass a held or uncertain delivery. Use `read_claude_delivery` with the original message ID to observe `held`, `expired`, `refused`, or a late reply. A transport error after writing began also retains pending ownership. Different destination sessions remain independent. A process restart loses in-memory receipts: retain the original ID and inspect the existing recipient conversation before any resend; never treat an unknown ID as permission to retry.
400
416
 
401
- Both bridge status tools report the loaded source fingerprint and process identity. Source or Desktop-routing configuration changes make the running process stale and block new sends until reconnect, while Claude inbox/receipt reads remain available. This guard applies only after the version containing it has loaded; upgrading files cannot retrofit the guard into an older process already running.
417
+ Both bridge status tools report the loaded source fingerprint and process identity. Supervised workers read immutable snapshots while the supervisor observes updates in the configured installation. An update waits for safe idle before activation; inspect `autoReload` for pending work or a failed candidate. Direct-entry workers retain the stale-source guard and require reconnect after source changes. Desktop-routing configuration changes still block new sends until the correct configuration is loaded, while Claude inbox/receipt reads remain available.
402
418
 
403
419
  Claude's [inbound permission controls](https://code.claude.com/docs/en/cross-session-messaging#control-inbound-messages) remain authoritative, and their default rule is symmetric: when no `crossSessionInbound` value applies, a message is delivered only when the sender's attested class matches the recipient's. A recipient that bypasses permission prompts holds every sender that does not attest `bypass`; a recipient that prompts (default, `acceptEdits`, `auto`, `dontAsk`) holds every sender that attests `bypass`. The bridge attests the sender's true class from the verified Codex turn and never adjusts it to suit a recipient. Claude Desktop does not declare the `peer_inbound_approval` dialog kind to Claude Code, so a message held by a Desktop-hosted session has no approval button there; Claude Code keeps it until `dialogExpiry` (five minutes by default) and then reports it as expired. A `held` receipt in Desktop mode therefore means the two tasks run in different permission classes, not that the sender was misread. Report that to the user: only they can run the recipient task in the matching class or set `crossSessionInbound` to `accept` in that session's own settings. Because that outcome is predictable, `send_to_claude_session` first reads the recipient session's effective `crossSessionInbound` from the same files Claude Desktop loads (managed settings, then the user's `~/.claude/settings.json`, with the recipient project's `.claude/settings.json` and `settings.local.json` able only to tighten it) and refuses at preflight with `CLAUDE_RECIPIENT_INBOUND_POLICY` when that value is `refuse` or `hold`, since nothing could reach Claude or be approved. An explicit `accept` sends without a class check, because Claude delivers regardless of class. Otherwise the parity default applies: the recipient task's permission mode from Claude Desktop metadata is compared with the verified sender class and a mismatch is refused with `CLAUDE_RECIPIENT_CLASS_MISMATCH`, naming both classes and the user-only remedies on either side; both checks repeat immediately before writing. A task whose mode is unknown is sent as before and Claude decides. Flags passed at launch such as `--settings` are invisible to the bridge, so put the value in a settings file. `list_claude_sessions` shows an explicit inbound value with its source, and receipts carry `senderApprovalPolicy`, `recipientPermissionMode`, `recipientPermissionClass`, and `recipientInboundPolicy`; a held receipt repeats both classes. Do not change `CLAUDE_BRIDGE_PERMISSION_MODE`, fabricate a sender class, or edit recipient settings on the user's behalf. Legacy CLI configuration must truthfully represent every session using that MCP entry; Desktop sends derive their class from the verified calling turn. If the Desktop UI does not expose the pending approval, keep the original receipt rather than creating a CLI session.
404
420
 
@@ -442,7 +458,7 @@ See [Claude Code cross-session messaging](https://code.claude.com/docs/en/cross-
442
458
 
443
459
  The relay has two hard limits in `src/claude-bridge.mjs`: at most **one forwarding attempt every 5s** and **50 per bridge run**. Replies arriving together are queued in order rather than discarded. Rebinding cannot reset the limit. Every queued reply keeps its original destination task and an inspectable forwarding status: `queued`, `sending`, `forwarded`, `failed`, `unknown`, or `blocked`. Read `read_claude_delivery` for a specific request or `read_claude_inbox` for late replies; status includes queue totals. A confirmed relay acknowledgement marks `forwarded`, while visibility in the Desktop conversation requires a separate UI check.
444
460
 
445
- Uncertain delivery is never retried automatically or routed through another backend. Replies blocked by the session limit remain readable with an explicit reason. Source updates block new prompts until reconnect but do not discard replies to already accepted prompts while their configured Desktop routing remains unchanged. The queue and receipts live in the current MCP process only: inspect pending messages before restarting; a new process does not prove that an old request failed. Two agents left talking to each other unattended still come to a stop.
461
+ Uncertain delivery is never retried automatically or routed through another backend. Replies blocked by the session limit remain readable with an explicit reason. Source updates defer worker replacement while deliveries or replies remain pending. The supervisor preserves the worker responsible for accepted prompts until it is safe to switch. A direct-entry worker instead blocks new prompts until reconnect after source changes. A manual restart still loses in-memory state: inspect pending messages before restarting; a new process does not prove that an old request failed. Two agents left talking to each other unattended still come to a stop.
446
462
 
447
463
  ## Tools — `codex-native-relay` (launched by Codex Desktop, macOS)
448
464
 
@@ -633,6 +649,9 @@ Runs the whole suite with `node --test`. It needs no Codex install, no login and
633
649
 
634
650
  | File | Covers |
635
651
  |---|---|
652
+ | `test/mcp-supervisor.test.mjs` | live reload of all three real MCP workers over unchanged client connections, immutable dependencies, rollback, pending deliveries, and no replay after crashes |
653
+ | `test/reload-control.test.mjs` | safe quiescence, retained receipts and account bindings, original task ownership, and forwarding limits |
654
+ | `test/native-relay-companion.test.mjs` | active socket handlers, startup races, listener handoff, and failed activation rollback |
636
655
  | `test/tool-contract.test.mjs` | all three servers boot over stdio and every tool declares a title, a description, per-parameter descriptions and complete annotation hints |
637
656
  | `test/server-requests.test.mjs` | all 10 app-server requests get a reply in the shape their schema declares — the regression test for "the turn pauses itself" |
638
657
  | `test/reconnect.test.mjs` | reconnect after a dropped socket, no leaked pending requests or listeners, an interrupted turn ending promptly, a refused first handshake being retried |
@@ -644,7 +663,7 @@ Runs the whole suite with `node --test`. It needs no Codex install, no login and
644
663
  | `test/native-relay.test.mjs` | the Codex Desktop relay: executor thread resolution, feature detection, Windows named-pipe and POSIX socket round trips, backend selection and fallback, and the companion answering a real MCP client that plays Codex Desktop |
645
664
  | `test/repo-hygiene.test.mjs` | no environment file or build output is ever tracked, versions do not drift, documentation stays in English |
646
665
 
647
- GitHub Actions runs the same command on every push and pull request, across Node 22 and 24 on Linux, macOS and Windows (`.github/workflows/ci.yml`). The Codex → Claude direction uses unix sockets on macOS/Linux and named pipes on Windows; the native relay has matching platform-specific coverage.
666
+ GitHub Actions runs the suite on every push and pull request, across Node 22 and 24 on Linux, macOS and Windows (`.github/workflows/ci.yml`). Windows runners limit concurrent test files to two so immutable snapshot copies do not starve the bounded PowerShell process-identity checks. The Codex → Claude direction uses unix sockets on macOS/Linux and named pipes on Windows; the native relay has matching platform-specific coverage.
648
667
 
649
668
  Two checks need a real Codex and are not part of `npm test`:
650
669
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minhspark/codex-mcp-bridge",
3
- "version": "1.13.9",
3
+ "version": "1.15.0",
4
4
  "description": "Two-way MCP bridge between Claude and Codex: prompts into a live Codex thread, messages into a running Claude Code session.",
5
5
  "keywords": [
6
6
  "mcp",
@@ -22,11 +22,11 @@
22
22
  },
23
23
  "type": "module",
24
24
  "bin": {
25
- "codex-mcp-bridge": "src/index.mjs",
26
- "claude-mcp-bridge": "src/claude-bridge.mjs",
25
+ "codex-mcp-bridge": "src/codex-bridge-launcher.mjs",
26
+ "claude-mcp-bridge": "src/claude-bridge-launcher.mjs",
27
27
  "codex-mcp-bridge-install": "scripts/install-claude-desktop.mjs",
28
28
  "claude-mcp-bridge-install": "scripts/install-codex-mcp.mjs",
29
- "codex-native-relay": "src/native-relay-companion.mjs",
29
+ "codex-native-relay": "src/native-relay-launcher.mjs",
30
30
  "codex-native-relay-install": "scripts/install-native-relay.mjs",
31
31
  "codex-npm-footer-install": "scripts/install-npm-footer.mjs"
32
32
  },
@@ -41,8 +41,8 @@
41
41
  "access": "public"
42
42
  },
43
43
  "scripts": {
44
- "start": "node src/index.mjs",
45
- "claude-bridge": "node src/claude-bridge.mjs",
44
+ "start": "node src/codex-bridge-launcher.mjs",
45
+ "claude-bridge": "node src/claude-bridge-launcher.mjs",
46
46
  "app-server": "codex app-server --listen ws://127.0.0.1:8791",
47
47
  "test": "node --test",
48
48
  "check": "node scripts/check.mjs",
@@ -6,6 +6,7 @@ import { fileURLToPath } from "node:url";
6
6
  import { IS_WINDOWS, PLATFORM_LABEL, claudeDesktopConfigPath, resolveCodexBin } from "../src/platform.mjs";
7
7
  import { desktopTasksConfigured } from "../src/native-relay.mjs";
8
8
  import { exitForVersionRequest } from "../src/cli-version.mjs";
9
+ import { createReleaseSnapshot, snapshotRoot } from "../src/release-snapshot.mjs";
9
10
 
10
11
  exitForVersionRequest(import.meta.url);
11
12
 
@@ -51,7 +52,8 @@ cfg.mcpServers = cfg.mcpServers ?? {};
51
52
  * already says, then the default. `--reset` drops the middle one for the rare
52
53
  * case of wanting the defaults back.
53
54
  */
54
- const previousEnv = (reset ? {} : cfg.mcpServers["codex-bridge"]?.env) ?? {};
55
+ const previousEntry = (reset ? {} : cfg.mcpServers["codex-bridge"]) ?? {};
56
+ const previousEnv = previousEntry.env ?? {};
55
57
  const settled = (name, fallback) => process.env[name] ?? previousEnv[name] ?? fallback;
56
58
  const desktopMode = settled("CODEX_BRIDGE_DESKTOP_TASKS", desktopTasksConfigured() ? "1" : "0");
57
59
 
@@ -60,8 +62,9 @@ const kept = Object.keys(previousEnv).filter(
60
62
  );
61
63
 
62
64
  cfg.mcpServers["codex-bridge"] = {
65
+ ...previousEntry,
63
66
  command: nodeBin,
64
- args: [path.join(root, "src", "index.mjs")],
67
+ args: [path.join(root, "src", "mcp-supervisor.mjs"), "index.mjs"],
65
68
  env: {
66
69
  /**
67
70
  * Keys this installer knows nothing about survive too - a setting added by
@@ -100,9 +103,14 @@ cfg.mcpServers["codex-bridge"] = {
100
103
  : {}),
101
104
  ...(process.env.CODEX_BRIDGE_MODEL ? { CODEX_BRIDGE_MODEL: process.env.CODEX_BRIDGE_MODEL } : {}),
102
105
  ...(process.env.CODEX_BRIDGE_EFFORT ? { CODEX_BRIDGE_EFFORT: process.env.CODEX_BRIDGE_EFFORT } : {}),
106
+ ...(process.env.CLAUDE_DESKTOP_USER_DATA !== undefined
107
+ ? { CLAUDE_DESKTOP_USER_DATA: process.env.CLAUDE_DESKTOP_USER_DATA }
108
+ : {}),
103
109
  },
104
110
  };
105
111
 
112
+ createReleaseSnapshot(root, { cache: snapshotRoot({ ...process.env, ...cfg.mcpServers["codex-bridge"].env }) });
113
+
106
114
  if (fs.existsSync(cfgPath)) {
107
115
  const backup = `${cfgPath}.bak-${new Date().toISOString().slice(0, 10)}-codexbridge`;
108
116
  if (!fs.existsSync(backup)) fs.copyFileSync(cfgPath, backup);
@@ -139,4 +147,5 @@ if (cfg.mcpServers["codex-bridge"].env.CODEX_BRIDGE_THREAD_POLICY === "owned") {
139
147
  );
140
148
  }
141
149
 
142
- console.log("\nRestart Claude Desktop to load the bridge.");
150
+ console.log("\nReconnect codex-bridge once in the existing Claude task to load the supervisor. Subsequent compatible installed-source updates reload automatically when the worker is safely idle.");
151
+ console.log("Verify codex_bridge_status from that task: autoReload must be enabled and runtime state must be current. A separate diagnostic process does not verify the app's loaded MCP process.");
@@ -8,13 +8,15 @@ import { PLATFORM_LABEL, resolveCodexBin, spawnEnv } from "../src/platform.mjs";
8
8
  import { exitForVersionRequest } from "../src/cli-version.mjs";
9
9
  import { desktopTasksConfigured } from "../src/native-relay.mjs";
10
10
  import { codexMcpRegistration } from "../src/codex-mcp-registration.mjs";
11
+ import { createReleaseSnapshot, snapshotRoot } from "../src/release-snapshot.mjs";
11
12
 
12
13
  exitForVersionRequest(import.meta.url);
13
14
 
14
15
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
15
16
  const codexBin = resolveCodexBin(process.env.CODEX_EXE);
16
17
  const serverName = process.env.CLAUDE_BRIDGE_NAME ?? "claude-bridge";
17
- const entry = path.join(root, "src", "claude-bridge.mjs");
18
+ const entry = path.join(root, "src", "mcp-supervisor.mjs");
19
+ const entryArgs = ["claude-bridge.mjs"];
18
20
  const remove = process.argv.includes("--remove");
19
21
  const permissionMode = process.env.CLAUDE_BRIDGE_PERMISSION_MODE;
20
22
  if (!remove && permissionMode && !["bypass", "prompting"].includes(permissionMode)) throw new Error("CLAUDE_BRIDGE_PERMISSION_MODE must be bypass or prompting");
@@ -35,13 +37,14 @@ const servers = JSON.parse(run(["mcp", "list", "--json"]));
35
37
  if (!Array.isArray(servers)) throw new Error("Codex returned an invalid MCP inventory; existing configuration was not changed");
36
38
  const existing = servers.some((server) => server.name === serverName)
37
39
  ? JSON.parse(run(["mcp", "get", serverName, "--json"])) : null;
38
- const registration = codexMcpRegistration({ name: serverName, existing, node: process.execPath, entry, env: process.env, desktopOnly: desktopTasksConfigured() });
40
+ const registration = codexMcpRegistration({ name: serverName, existing, node: process.execPath, entry, entryArgs, env: process.env, desktopOnly: desktopTasksConfigured() });
41
+ createReleaseSnapshot(root, { cache: snapshotRoot({ ...process.env, ...registration.environment }) });
39
42
  run(registration.args);
40
43
 
41
44
  console.log(`platform: ${PLATFORM_LABEL}`);
42
45
  console.log(`registered MCP server "${serverName}" with Codex:`);
43
- console.log(`command: ${process.execPath} ${entry}`);
46
+ console.log(`command: ${process.execPath} ${entry} ${entryArgs.join(" ")}`);
44
47
  console.log(`environment keys: ${registration.environmentKeys.join(", ")}`);
45
- console.log("\nReconnect the MCP server in the existing Codex Desktop task, or restart the app and reopen that same task. Do not create a replacement task.");
46
- console.log("Verify claude_bridge_status from that task: runtime state must be current and the session policy must match this installation. A separate diagnostic process does not verify the app's loaded MCP process.");
48
+ console.log("\nReconnect claude-bridge once in the existing Codex Desktop task to load the supervisor. Subsequent compatible installed-source updates reload automatically when the worker is safely idle. Do not create a replacement task.");
49
+ console.log("Verify claude_bridge_status from that task: autoReload must be enabled, runtime state must be current, and the session policy must match this installation. A separate diagnostic process does not verify the app's loaded MCP process.");
47
50
  console.log(`remove: node scripts/install-codex-mcp.mjs --remove`);
@@ -17,12 +17,15 @@ import {
17
17
  spawnEnv,
18
18
  } from "../src/platform.mjs";
19
19
  import { exitForVersionRequest } from "../src/cli-version.mjs";
20
+ import { stdioMcpRegistration } from "../src/codex-mcp-registration.mjs";
21
+ import { createReleaseSnapshot, snapshotRoot } from "../src/release-snapshot.mjs";
20
22
 
21
23
  exitForVersionRequest(import.meta.url);
22
24
 
23
- const VERSION = "1.13.9";
25
+ const VERSION = "1.15.0";
24
26
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
25
- const entry = path.join(root, "src", "native-relay-companion.mjs");
27
+ const entry = path.join(root, "src", "mcp-supervisor.mjs");
28
+ const entryArgs = ["native-relay-companion.mjs"];
26
29
  const serverName = process.env.CODEX_NATIVE_RELAY_NAME ?? "codex-native-relay";
27
30
  const remove = process.argv.includes("--remove");
28
31
  const skipBootstrap = process.argv.includes("--no-bootstrap");
@@ -60,11 +63,10 @@ if (!IS_MACOS && !IS_WINDOWS) {
60
63
  console.log("claude-bridge will keep using the app-server path here.");
61
64
  }
62
65
 
63
- try {
64
- run(["mcp", "remove", serverName]);
65
- } catch {
66
- // not registered yet
67
- }
66
+ const servers = JSON.parse(run(["mcp", "list", "--json"]));
67
+ if (!Array.isArray(servers)) throw new Error("Codex returned an invalid MCP inventory; existing configuration was not changed");
68
+ const existingServer = servers.some((server) => server.name === serverName)
69
+ ? JSON.parse(run(["mcp", "get", serverName, "--json"])) : null;
68
70
  /**
69
71
  * Registering under whichever Node happens to run this installer is what made
70
72
  * the companion unreachable on macOS: Codex Desktop rejects a peer whose
@@ -75,7 +77,11 @@ try {
75
77
  */
76
78
  const runtime = resolveCodexDesktopNodeBin();
77
79
 
78
- run(["mcp", "add", serverName, "--", runtime.path, entry]);
80
+ const registration = stdioMcpRegistration({ name: serverName, existing: existingServer, node: runtime.path, entry, entryArgs, envOverrides: {
81
+ ...(process.env.CLAUDE_DESKTOP_USER_DATA !== undefined ? { CLAUDE_DESKTOP_USER_DATA: process.env.CLAUDE_DESKTOP_USER_DATA } : {}),
82
+ } });
83
+ createReleaseSnapshot(root, { cache: snapshotRoot({ ...process.env, ...registration.environment }) });
84
+ run(registration.args);
79
85
 
80
86
  console.log(`platform: ${PLATFORM_LABEL}`);
81
87
  console.log(`relay runtime: ${runtime.path} (${runtime.source})`);
@@ -120,5 +126,5 @@ if (process.argv.includes("--desktop-tasks")) {
120
126
  writeRelayConfig({ ...readRelayConfig(), desktopTasks: true });
121
127
  console.log("Desktop task creation enabled: exact saved local projects, immediate visibility, Codex Desktop permissions.");
122
128
  }
123
- console.log("Restart Codex Desktop so it launches the companion, then check with native_relay_status.");
129
+ console.log("Reconnect codex-native-relay once in the existing Codex Desktop task to load the supervisor, then verify native_relay_status reports autoReload enabled. Subsequent compatible installed-source updates reload automatically when safely idle.");
124
130
  console.log("remove: node scripts/install-native-relay.mjs --remove");
@@ -0,0 +1,77 @@
1
+ import { readClaudeAccountContext } from "./desktop-account-context.mjs";
2
+ import { readCodexAccountContext } from "./codex-account-context.mjs";
3
+ import { preflightFailure } from "./recipient-preflight.mjs";
4
+ import { assertClaudeSessionProcess, listClaudeSessions } from "./peer-protocol.mjs";
5
+ import { readClaudeDesktopContext } from "./claude-desktop-context.mjs";
6
+
7
+ const unsolicitedAccountInspections = new WeakSet();
8
+ const MAX_SESSION_CANDIDATES = 8192;
9
+
10
+ export function readBridgeAccounts() {
11
+ return { claude: readClaudeAccountContext(), codex: readCodexAccountContext() };
12
+ }
13
+
14
+ export function accountIdentity(accounts) {
15
+ if (["claude", "codex"].some((provider) => accounts?.[provider]?.status !== "verified"
16
+ || typeof accounts[provider].fingerprint !== "string" || !accounts[provider].fingerprint)) return null;
17
+ return Object.freeze({ claude: accounts.claude.fingerprint, codex: accounts.codex.fingerprint });
18
+ }
19
+
20
+ export function sameAccountIdentity(expected, accounts) {
21
+ const current = accountIdentity(accounts);
22
+ return Boolean(expected?.claude && expected?.codex && current
23
+ && expected.claude === current.claude && expected.codex === current.codex);
24
+ }
25
+
26
+ export function requireBridgeAccounts(accounts) {
27
+ for (const provider of ["claude", "codex"]) {
28
+ if (accounts?.[provider]?.status !== "verified" || typeof accounts[provider].fingerprint !== "string" || !accounts[provider].fingerprint) {
29
+ throw preflightFailure("BRIDGE_ACCOUNT_UNVERIFIED", `${provider === "claude" ? "Claude" : "Codex"} account is ${accounts?.[provider]?.status ?? "unavailable"}: ${accounts?.[provider]?.reason ?? "No current account evidence"}. Finish signing in; the next call will discover the account again.`);
30
+ }
31
+ }
32
+ return accountIdentity(accounts);
33
+ }
34
+
35
+ export function assertAccountIdentity(expected, accounts = readBridgeAccounts()) {
36
+ if (!sameAccountIdentity(expected, accounts)) {
37
+ const error = preflightFailure("BRIDGE_ACCOUNT_CHANGED", "The Claude or Codex account changed or signed out while this operation was pending. Its original destination was preserved; discover the current account before sending new work.");
38
+ error.code = "BRIDGE_ACCOUNT_CHANGED";
39
+ throw error;
40
+ }
41
+ }
42
+
43
+ export function publicAccountState(account) {
44
+ return { status: account?.status ?? "unavailable", fingerprint: account?.fingerprint ?? null, reason: account?.reason ?? "No account evidence" };
45
+ }
46
+
47
+ export function bindUnsolicitedClaudeMessageAccount(record, {
48
+ readAccounts = readBridgeAccounts,
49
+ listSessions = listClaudeSessions,
50
+ assertProcess = assertClaudeSessionProcess,
51
+ readContext = (session, account) => readClaudeDesktopContext(session, { account }),
52
+ hasPendingSource = () => false,
53
+ } = {}) {
54
+ if (!record || typeof record !== "object" || record.accountContext != null || record.inReplyTo != null
55
+ || unsolicitedAccountInspections.has(record)) return false;
56
+ unsolicitedAccountInspections.add(record);
57
+ if (typeof record.fromSocket !== "string" || !record.fromSocket) return false;
58
+ try {
59
+ if (hasPendingSource(record.fromSocket)) return false;
60
+ const accounts = readAccounts();
61
+ const selected = accountIdentity(accounts);
62
+ if (!selected) return false;
63
+ const sessions = listSessions();
64
+ if (!Array.isArray(sessions) || sessions.length > MAX_SESSION_CANDIDATES) return false;
65
+ const matches = sessions.filter((session) => session?.alive === true && session.socket === record.fromSocket);
66
+ if (matches.length !== 1 || matches[0].entrypoint !== "claude-desktop") return false;
67
+ const session = matches[0];
68
+ assertProcess(session);
69
+ const context = readContext(session, accounts.claude);
70
+ if (context?.status !== "matched" || context.accountFingerprint !== selected.claude) return false;
71
+ if (!sameAccountIdentity(selected, readAccounts())) return false;
72
+ record.accountContext = selected;
73
+ return true;
74
+ } catch {
75
+ return false;
76
+ }
77
+ }
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { runSupervisor } from "./mcp-supervisor.mjs";
3
+ await runSupervisor("claude-bridge.mjs");