@minhspark/codex-mcp-bridge 1.15.2 → 1.16.1

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.
@@ -2,15 +2,17 @@
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { z } from "zod";
5
+ import { createHash } from "node:crypto";
5
6
 
6
7
  import { CodexAppServerClient } from "./app-server-client.mjs";
7
- import { PLATFORM_LABEL } from "./platform.mjs";
8
- import { PeerEndpoint, assertClaudeSessionCwd, assertClaudeSessionProcess, findClaudeSession, listClaudeSessions, readTranscript } from "./peer-protocol.mjs";
8
+ import { PLATFORM_LABEL, openClaudeCodeComposer } from "./platform.mjs";
9
+ import { PeerEndpoint, assertClaudeSessionCwd, assertClaudeSessionProcess, findClaudeSession, listClaudeSessions, readTranscript, readInitialTranscriptMessage } from "./peer-protocol.mjs";
9
10
  import { createThreadDelivery } from "./thread-delivery.mjs";
10
11
  import { exitForVersionRequest } from "./cli-version.mjs";
11
12
  import { desktopTasksConfigured } from "./native-relay.mjs";
12
13
  import { createRuntimeState } from "./runtime-state.mjs";
13
- import { readClaudeDesktopContext } from "./claude-desktop-context.mjs";
14
+ import { readClaudeDesktopContext, readClaudeDesktopTasks } from "./claude-desktop-context.mjs";
15
+ import { ClaudeSessionCreation } from "./claude-session-creation.mjs";
14
16
  import { readClaudeInboundPolicy } from "./claude-inbound-policy.mjs";
15
17
  import { assertRecipientClass, preflightFailure } from "./recipient-preflight.mjs";
16
18
  import { readCodexSenderContext } from "./codex-sender-context.mjs";
@@ -18,11 +20,13 @@ import { ReplyForwarder } from "./reply-forwarder.mjs";
18
20
  import { assertRoutingReload, clientReloadReason, createReloadControl } from "./reload-control.mjs";
19
21
  import { readClaudeAccountContext } from "./desktop-account-context.mjs";
20
22
  import { assertAccountIdentity, bindUnsolicitedClaudeMessageAccount, publicAccountState, readBridgeAccounts, requireBridgeAccounts, sameAccountIdentity } from "./bridge-account-context.mjs";
21
- import { resolveClaudeDesktopSession } from "./claude-session-router.mjs";
23
+ import { resolveClaudeDesktopSession, sameClaudeDesktopRecipient } from "./claude-session-router.mjs";
24
+ import { createHardenedRootPolicy } from "./hardened-root-policy.mjs";
22
25
 
23
26
  exitForVersionRequest(import.meta.url);
24
27
 
25
- const VERSION = "1.15.2";
28
+ const VERSION = "1.16.1";
29
+ void import("./telemetry.mjs").then(({ startUsageReporting }) => startUsageReporting({ version: VERSION })).catch(() => {});
26
30
  const FORWARD_MIN_INTERVAL_MS = 5000;
27
31
  const FORWARD_MAX_PER_SESSION = 50;
28
32
 
@@ -30,6 +34,7 @@ const log = (msg) => process.stderr.write(`[claude-bridge] ${msg}\n`);
30
34
 
31
35
  const defaultPeerName = process.env.CLAUDE_BRIDGE_PEER_NAME ?? `codex-${process.pid}`;
32
36
  const desktopOnly = desktopTasksConfigured();
37
+ const rootPolicy = createHardenedRootPolicy();
33
38
  const runtime = createRuntimeState({ configuration: desktopTasksConfigured });
34
39
 
35
40
  const peer = new PeerEndpoint({
@@ -55,6 +60,35 @@ const forwarding = {
55
60
  threadId: process.env.CODEX_THREAD_ID ?? null,
56
61
  };
57
62
 
63
+ const creationRootBindings = new Map();
64
+
65
+ const creations = new ClaudeSessionCreation({
66
+ open: openClaudeCodeComposer,
67
+ listTasks: (account) => readClaudeDesktopTasks({ account }).filter((task) => rootPolicy.allows(task.cwd)),
68
+ listSessions: () => listClaudeSessions().filter((session) => rootPolicy.allows(session.cwd)),
69
+ readContext: (session, account) => readClaudeDesktopContext(session, { account }),
70
+ readTranscript: (...args) => {
71
+ const binding = rootPolicy.capture(args[1], "Claude creation transcript working directory");
72
+ const transcript = readInitialTranscriptMessage(...args);
73
+ rootPolicy.recheck(binding, "Claude creation transcript working directory");
74
+ return transcript;
75
+ },
76
+ readAccount: readClaudeAccountContext,
77
+ assertProcess: assertClaudeSessionProcess,
78
+ });
79
+ function recheckScopeBindings(bindings) {
80
+ if (!rootPolicy.enabled) return;
81
+ if (!bindings?.sender || !bindings?.recipient) throw Object.assign(new Error("Hardened bridge record has no original sender and recipient root binding"), { code: "ROOT_BINDING_MISSING" });
82
+ rootPolicy.recheck(bindings.sender, "Original Codex sender working directory");
83
+ rootPolicy.recheck(bindings.recipient, "Original Claude recipient working directory");
84
+ }
85
+
86
+ function recordVisible(record, accounts = readBridgeAccounts()) {
87
+ if (desktopOnly && !sameAccountIdentity(record?.accountContext, accounts)) return false;
88
+ if (!rootPolicy.enabled) return true;
89
+ try { recheckScopeBindings(record?.scopeBindings); return true; } catch { return false; }
90
+ }
91
+
58
92
  const replyForwarder = new ReplyForwarder({
59
93
  minIntervalMs: FORWARD_MIN_INTERVAL_MS,
60
94
  maxPerSession: FORWARD_MAX_PER_SESSION,
@@ -63,16 +97,17 @@ const replyForwarder = new ReplyForwarder({
63
97
  throw Object.assign(new Error("Bridge routing changed; the reply was not forwarded. Inspect the original receipt before reconnecting."), { code: "REPLY_ROUTING_CHANGED" });
64
98
  }
65
99
  if (desktopOnly) assertAccountIdentity(record.accountContext);
100
+ recheckScopeBindings(record.scopeBindings);
66
101
  },
67
102
  deliver: (threadId, record) => delivery.deliver(threadId, `[message from Claude session ${record.fromSocket ?? "?"}]\n${record.absorbed ? "[This reply is the closing text of a turn that absorbed the message while it was running; it may not address the message.]\n" : ""}\n${record.text}`,
68
- desktopOnly ? { beforeSend: () => assertAccountIdentity(record.accountContext), accountContext: record.accountContext } : {}),
103
+ desktopOnly ? { beforeSend: () => { assertAccountIdentity(record.accountContext); recheckScopeBindings(record.scopeBindings); }, accountContext: record.accountContext } : {}),
69
104
  });
70
105
 
71
106
  const reload = createReloadControl({
72
107
  entry: "claude-bridge.mjs",
73
108
  inspect: () => peer.reloadReason() ?? replyForwarder.reloadReason() ?? clientReloadReason(codex),
74
109
  quiesce: async () => { await peer.quiesce(); codex.close(); },
75
- exportState: () => ({ desktopOnly, peer: peer.exportReloadState(), forwarding: replyForwarder.exportReloadState(), threadId: forwarding.threadId }),
110
+ exportState: () => ({ desktopOnly, peer: peer.exportReloadState(), forwarding: replyForwarder.exportReloadState(), threadId: forwarding.threadId, creations: creations.exportState(), creationRootBindings: [...creationRootBindings] }),
76
111
  restore: (state) => {
77
112
  assertRoutingReload(state.desktopOnly, desktopOnly);
78
113
  if (state.threadId !== null && (typeof state.threadId !== "string" || !state.threadId.trim())) {
@@ -81,6 +116,14 @@ const reload = createReloadControl({
81
116
  peer.restoreReloadState(state.peer);
82
117
  replyForwarder.restoreReloadState(state.forwarding);
83
118
  forwarding.threadId = state.threadId;
119
+ if (state.creations !== undefined) creations.restoreState(state.creations);
120
+ if (state.creationRootBindings !== undefined) {
121
+ if (!Array.isArray(state.creationRootBindings) || state.creationRootBindings.length > 32) throw new Error("Invalid Claude creation root bindings");
122
+ for (const entry of state.creationRootBindings) {
123
+ if (!Array.isArray(entry) || entry.length !== 2 || typeof entry[0] !== "string" || creationRootBindings.has(entry[0]) || !creations.requests.has(entry[0])) throw new Error("Invalid Claude creation root binding");
124
+ creationRootBindings.set(entry[0], entry[1]);
125
+ }
126
+ }
84
127
  },
85
128
  activate: () => peer.start(),
86
129
  resume: () => peer.resume(),
@@ -89,6 +132,9 @@ const reload = createReloadControl({
89
132
  function readReceipt(msgId) {
90
133
  const receipt = peer.readDelivery(msgId);
91
134
  if (desktopOnly && receipt && !sameAccountIdentity(receipt.accountContext, readBridgeAccounts())) return null;
135
+ if (rootPolicy.enabled) {
136
+ try { recheckScopeBindings(receipt?.scopeBindings); } catch { return null; }
137
+ }
92
138
  return receipt ? { ...receipt, forwarding: replyForwarder.read(msgId) ?? receipt.forwardingError ?? null } : null;
93
139
  }
94
140
 
@@ -117,6 +163,10 @@ function withDesktopContext(session, account = readClaudeAccountContext()) {
117
163
  ? { ...session, desktop: readClaudeDesktopContext(session, { account }), inbound: readClaudeInboundPolicy(session.cwd) } : session;
118
164
  }
119
165
 
166
+ function assertSessionRoot(session, label = "Claude session") {
167
+ return rootPolicy.assert(session?.cwd, `${label} working directory`);
168
+ }
169
+
120
170
  function assertDesktopTask(session, expectedTaskId) {
121
171
  if (session.desktop?.status !== "matched") {
122
172
  throw preflightFailure("CLAUDE_DESKTOP_TASK_UNVERIFIED", session.desktop?.reason ?? "The live session could not be matched to a Claude Desktop task.");
@@ -134,6 +184,47 @@ function assertSender(meta) {
134
184
  return sender;
135
185
  }
136
186
 
187
+ function assertCreationRoots(requestId, sender) {
188
+ if (!rootPolicy.enabled) return;
189
+ rootPolicy.assert(sender.cwd, "Codex sender working directory");
190
+ recheckScopeBindings(creationRootBindings.get(requestId));
191
+ }
192
+
193
+ async function startClaudeCreation({ requestId, cwd, prompt }, meta) {
194
+ runtime.assertCurrent();
195
+ if (!desktopOnly) throw new Error("New Claude sessions require Desktop-only mode");
196
+ const sender = assertSender(meta);
197
+ const accounts = readBridgeAccounts();
198
+ const selectedAccounts = requireBridgeAccounts(accounts);
199
+ if (rootPolicy.enabled && creations.requests.has(requestId)) assertCreationRoots(requestId, sender);
200
+ const scopeBindings = rootPolicy.enabled ? creationRootBindings.get(requestId) ?? {
201
+ sender: rootPolicy.capture(sender.cwd, "Codex sender working directory"),
202
+ recipient: rootPolicy.capture(cwd, "Claude creation working directory"),
203
+ } : null;
204
+ const receipt = await creations.start({ requestId, cwd, prompt, account: accounts.claude, senderThreadId: sender.threadId,
205
+ beforeOpen: () => {
206
+ runtime.assertCurrent();
207
+ assertAccountIdentity(selectedAccounts);
208
+ recheckScopeBindings(scopeBindings);
209
+ if (rootPolicy.enabled && !rootPolicy.same(cwd, scopeBindings.recipient.path)) throw new Error("The Claude creation working directory changed before opening Desktop");
210
+ const current = assertSender(meta);
211
+ if (JSON.stringify(current) !== JSON.stringify(sender)) throw new Error("The calling Codex turn changed before opening Claude Desktop");
212
+ },
213
+ });
214
+ if (rootPolicy.enabled) creationRootBindings.set(requestId, scopeBindings);
215
+ return { ...textResult(JSON.stringify(receipt, null, 2)), structuredContent: { creation: receipt } };
216
+ }
217
+
218
+ async function inspectClaudeCreation(requestId, meta) {
219
+ const sender = assertSender(meta);
220
+ assertCreationRoots(requestId, sender);
221
+ const accounts = readBridgeAccounts();
222
+ requireBridgeAccounts(accounts);
223
+ const receipt = await creations.inspect(requestId, { account: accounts.claude, senderThreadId: sender.threadId });
224
+ assertCreationRoots(requestId, sender);
225
+ return { ...textResult(JSON.stringify(receipt, null, 2)), structuredContent: { creation: receipt } };
226
+ }
227
+
137
228
  function forwardToCodexThread(record) {
138
229
  const threadId = record.replyThreadId ?? (desktopOnly ? null : forwarding.threadId);
139
230
  if (!threadId) return;
@@ -146,6 +237,9 @@ function forwardToCodexThread(record) {
146
237
  }
147
238
 
148
239
  peer.onMessage((record) => {
240
+ if (rootPolicy.enabled && !record.cwd) { record.forwardingError = { status: "blocked", reasonCode: "ROOT_UNAUTHORIZED", reason: "Unsolicited inbound messages are disabled in hardened mode without a verified root binding" }; return; }
241
+ try { if (rootPolicy.enabled) rootPolicy.assert(record.cwd, "Inbound Claude message working directory"); }
242
+ catch (err) { record.forwardingError = { status: "blocked", reasonCode: "ROOT_UNAUTHORIZED", reason: err.message }; return; }
149
243
  if (desktopOnly) bindUnsolicitedClaudeMessageAccount(record, {
150
244
  hasPendingSource: (socket) => [...peer.pendingMessages.values()].some((pending) => pending.targetSocket === socket),
151
245
  });
@@ -178,6 +272,50 @@ function registerTool(name, definition, handler) {
178
272
  });
179
273
  }
180
274
 
275
+ registerTool(
276
+ "start_claude_session",
277
+ {
278
+ title: "Prepare a new Claude Desktop Code session",
279
+ description: "Use only when the user explicitly requests a NEW Claude Desktop session. Open the official Code composer in an existing project with a prefilled prompt. Claude Desktop requires the user to confirm the folder and press Send; opening the link is not session creation. Retain requestId and inspect read_claude_creation. Never substitute an existing task or launch a CLI session.",
280
+ inputSchema: {
281
+ requestId: z.string().describe("Stable UUID for this creation; reuse exactly when inspecting or retrying"),
282
+ cwd: z.string().describe("Exact absolute directory of the existing Claude Desktop project"),
283
+ prompt: z.string().describe("Initial prompt for the NEW conversation; shown for user confirmation"),
284
+ },
285
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
286
+ },
287
+ async (args, extra) => startClaudeCreation(args, extra?._meta),
288
+ );
289
+
290
+ registerTool(
291
+ "read_claude_creation",
292
+ {
293
+ title: "Verify a new Claude Desktop session",
294
+ description: "Inspect an existing creation request without reopening or sending. A created receipt requires a new native task, matching account/project, live process identity, and the exact initial user prompt. awaiting_project_confirmation means the prompt arrived in a new task but its folder still differs; the user must adopt the requested project before creation can be confirmed. An old task resumed with a new process never counts as a new session.",
295
+ inputSchema: { requestId: z.string().describe("Original requestId returned by start_claude_session") },
296
+ annotations: { readOnlyHint: true, openWorldHint: false },
297
+ },
298
+ async ({ requestId }, extra) => inspectClaudeCreation(requestId, extra?._meta),
299
+ );
300
+
301
+ registerTool(
302
+ "abandon_claude_creation",
303
+ {
304
+ title: "Release a cancelled Claude creation request",
305
+ description: "Use only when the user explicitly cancels a pending creation request. Release its composer reservation while retaining the receipt. This does not close Claude Desktop or prove that the prefilled prompt was never submitted; the old request remains inspectable.",
306
+ inputSchema: { requestId: z.string().describe("Exact creation requestId the user cancelled") },
307
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
308
+ },
309
+ async ({ requestId }, extra) => {
310
+ const sender = assertSender(extra?._meta);
311
+ assertCreationRoots(requestId, sender);
312
+ const accounts = readBridgeAccounts();
313
+ requireBridgeAccounts(accounts);
314
+ const receipt = await creations.abandon(requestId, { account: accounts.claude, senderThreadId: sender.threadId });
315
+ return { ...textResult(JSON.stringify(receipt, null, 2)), structuredContent: { creation: receipt } };
316
+ },
317
+ );
318
+
181
319
  registerTool(
182
320
  "list_claude_sessions",
183
321
  {
@@ -196,11 +334,13 @@ registerTool(
196
334
  },
197
335
  async ({ includeDead, expectedCwd }) => {
198
336
  try {
337
+ if (rootPolicy.enabled && expectedCwd !== undefined) rootPolicy.assert(expectedCwd, "Requested working directory");
199
338
  if (expectedCwd !== undefined) assertClaudeSessionCwd({ cwd: expectedCwd }, expectedCwd);
200
339
  const account = readClaudeAccountContext();
201
340
  if (desktopOnly && account.status !== "verified") return { ...textResult(`Claude account ${account.status}: ${account.reason} Sign in and retry discovery.`, true), structuredContent: { account: publicAccountState(account), sessions: [] } };
202
341
  const sessions = listClaudeSessions({ includeDead: includeDead ?? false }).filter(
203
342
  (s) => s.pid !== process.pid && (!desktopOnly || s.entrypoint === "claude-desktop"),
343
+ ).filter((session) => !rootPolicy.enabled || rootPolicy.allows(session.cwd)
204
344
  ).filter((session) => {
205
345
  if (expectedCwd === undefined) return true;
206
346
  try { assertClaudeSessionCwd(session, expectedCwd); return true; }
@@ -221,13 +361,14 @@ registerTool(
221
361
  title: "Send a message to a Claude session",
222
362
  description:
223
363
  "Send a message to a running Claude Code session's peer transport and wait for its reply. " +
364
+ "Only when the user requests a NEW conversation, target new prepares the native Desktop composer in expectedCwd; it requires user confirmation and returns a creation receipt, not a delivery receipt. " +
224
365
  "A socket write alone does not confirm that Claude received the message. Set waitSec to 0 to send without confirmation. " +
225
366
  "Every send is refused while earlier messages to that session still await replies, including waitSec 0; " +
226
367
  "wait for those replies and read_claude_inbox before trying again. Desktop-only mode refuses CLI or unknown " +
227
368
  "entrypoints, partial names, ambiguous targets, and a missing or mismatched expectedCwd or expectedTaskId before sending. Use target auto and expectedCwd for automatic current-account discovery; expectedTaskId is optional only when auto finds exactly one matching task. " +
228
369
  "The sender's current permissions are verified per call; environment overrides and manual binding cannot bypass an unknown sender. It never creates a replacement session.",
229
370
  inputSchema: {
230
- target: z.string().describe("Use auto for current-account discovery in expectedCwd, or an exact session name, pid or sessionId"),
371
+ target: z.string().describe("Use auto for current-account discovery in expectedCwd, an exact session identity, or new to prepare a separate Desktop conversation requiring user confirmation"),
231
372
  message: z.string().describe("The message text to deliver"),
232
373
  expectedCwd: z.string().optional().describe("Exact absolute project directory independently verified by the caller; required in Desktop-only mode"),
233
374
  expectedTaskId: z.string().optional().describe("Exact native Claude Desktop task ID from list_claude_sessions; verify its title in the app before sending"),
@@ -249,6 +390,12 @@ registerTool(
249
390
  async ({ target, message, waitSec, expectedCwd, expectedTaskId }, extra) => {
250
391
  try {
251
392
  runtime.assertCurrent();
393
+ if (target === "new") {
394
+ if (expectedTaskId !== undefined) throw new Error("A new Claude conversation cannot target an existing task ID");
395
+ const sender = assertSender(extra?._meta);
396
+ const requestId = createHash("sha256").update(JSON.stringify([sender.threadId, sender.turnId, expectedCwd, message])).digest("hex");
397
+ return await startClaudeCreation({ requestId, cwd: expectedCwd, prompt: message }, extra?._meta);
398
+ }
252
399
  const accounts = desktopOnly ? readBridgeAccounts() : null;
253
400
  const selectedAccounts = desktopOnly ? requireBridgeAccounts(accounts) : null;
254
401
  const found = desktopOnly ? resolveClaudeDesktopSession({ target, expectedCwd, expectedTaskId,
@@ -257,6 +404,7 @@ registerTool(
257
404
  }) : findClaudeSession(target, { desktopOnly });
258
405
  if (!found) return textResult(desktopOnly ? missingDesktopSession : `No live Claude session matches "${target}".`, true);
259
406
  const session = desktopOnly ? { ...found, inbound: readClaudeInboundPolicy(found.cwd) } : withDesktopContext(found);
407
+ assertSessionRoot(session);
260
408
  const selectedTaskId = target === "auto" ? session.desktop?.taskId : expectedTaskId;
261
409
  if (desktopOnly || expectedCwd !== undefined) assertClaudeSessionCwd(session, expectedCwd);
262
410
  if (desktopOnly) {
@@ -264,6 +412,10 @@ registerTool(
264
412
  assertClaudeSessionProcess(session);
265
413
  }
266
414
  const sender = desktopOnly ? assertSender(extra?._meta) : null;
415
+ const scopeBindings = rootPolicy.enabled ? {
416
+ sender: rootPolicy.capture(sender?.cwd, "Codex sender working directory"),
417
+ recipient: rootPolicy.capture(session.cwd, "Claude recipient working directory"),
418
+ } : null;
267
419
  if (desktopOnly) assertRecipientClass(session, sender);
268
420
  await peer.start();
269
421
 
@@ -275,19 +427,28 @@ registerTool(
275
427
  ...(selectedAccounts ? { accountContext: selectedAccounts } : {}),
276
428
  ...(sender ? { permissionMode: sender.mode, replyThreadId: sender.threadId, senderReview: sender.review, senderApprovalPolicy: sender.approvalPolicy } : {}),
277
429
  ...(desktop ? { recipient: { permissionMode: session.desktop.permissionMode ?? null, permissionClass: session.desktop.permissionClass ?? null, inboundPolicy: session.inbound?.value ?? null } } : {}),
430
+ ...(scopeBindings ? { scopeBindings } : {}),
278
431
  beforeSend: () => {
279
432
  runtime.assertCurrent();
280
433
  if (desktopOnly) assertAccountIdentity(selectedAccounts);
281
434
  const current = findClaudeSession(session.sessionId ?? String(session.pid), { desktopOnly });
282
- if (!current || current.pid !== session.pid || current.socket !== session.socket) {
435
+ const destinationUnchanged = desktopOnly
436
+ ? sameClaudeDesktopRecipient(session, current)
437
+ : Boolean(current && current.pid === session.pid && current.socket === session.socket);
438
+ if (!destinationUnchanged) {
283
439
  throw new Error("The Claude destination changed while this message was queued. No message was sent; inspect the existing Desktop session.");
284
440
  }
285
441
  if (desktopOnly || expectedCwd !== undefined) assertClaudeSessionCwd(current, expectedCwd);
442
+ assertSessionRoot(current);
286
443
  if (desktopOnly) {
287
444
  assertClaudeSessionProcess(current);
288
445
  const refreshed = withDesktopContext(current, readClaudeAccountContext());
289
446
  assertDesktopTask(refreshed, selectedTaskId);
290
447
  const active = assertSender(extra?._meta);
448
+ if (rootPolicy.enabled) {
449
+ rootPolicy.recheck(scopeBindings.sender, "Codex sender working directory");
450
+ rootPolicy.recheck(scopeBindings.recipient, "Claude recipient working directory");
451
+ }
291
452
  if (active.threadId !== sender.threadId || active.turnId !== sender.turnId || active.cwd !== sender.cwd || active.mode !== sender.mode || active.approvalPolicy !== sender.approvalPolicy || JSON.stringify(active.review) !== JSON.stringify(sender.review)) {
292
453
  throw preflightFailure("CODEX_SENDER_CONTEXT_CHANGED", "The sender's active turn or permissions changed while this message was queued.");
293
454
  }
@@ -339,11 +500,12 @@ registerTool(
339
500
  "read_claude_delivery",
340
501
  {
341
502
  title: "Inspect a Claude message receipt without resending",
342
- description: "Read the latest recipient control receipt or correlated reply for a message sent by this MCP process. Unknown IDs do not prove non-delivery; retain the original receipt after a reconnect and inspect the existing Claude session before any resend.",
503
+ description: "Read the latest recipient control receipt or correlated reply for a message sent by this MCP process. Also accepts the 64-character requestId returned by target new to inspect creation without reopening. Unknown IDs do not prove non-delivery; retain the original receipt after a reconnect and inspect the existing Claude session before any resend.",
343
504
  inputSchema: { msgId: z.string().describe("The original message ID returned by send_to_claude_session") },
344
505
  annotations: { readOnlyHint: true, openWorldHint: false },
345
506
  },
346
- async ({ msgId }) => {
507
+ async ({ msgId }, extra) => {
508
+ if (/^[0-9a-f]{64}$/i.test(msgId)) return inspectClaudeCreation(msgId, extra?._meta);
347
509
  const receipt = readReceipt(msgId);
348
510
  if (!receipt) return textResult("This MCP process has no receipt for that message ID. It may belong to a previous process; do not infer failure or resend. Inspect the original Claude Desktop session.", true);
349
511
  return { ...textResult(JSON.stringify(receipt, null, 2)), structuredContent: { receipt } };
@@ -371,7 +533,7 @@ registerTool(
371
533
  try {
372
534
  await peer.start();
373
535
  const accounts = desktopOnly ? readBridgeAccounts() : null;
374
- const messages = peer.drainInbox(limit ?? 20, (record) => !desktopOnly || sameAccountIdentity(record.accountContext, accounts));
536
+ const messages = peer.drainInbox(limit ?? 20, (record) => recordVisible(record, accounts));
375
537
  if (!messages.length) return textResult(desktopOnly ? "No messages for the current accounts." : "Inbox is empty.");
376
538
  const result = textResult(
377
539
  messages
@@ -382,7 +544,8 @@ registerTool(
382
544
  })
383
545
  .join("\n\n"),
384
546
  );
385
- return { ...result, structuredContent: { messages: messages.map((record) => ({ ...record, forwarding: replyForwarder.read(record.inReplyTo ?? record.msgId) ?? record.forwardingError ?? null })), remaining: peer.inbox.length } };
547
+ const remaining = peer.inbox.filter((record) => recordVisible(record, accounts)).length;
548
+ return { ...result, structuredContent: { messages: messages.map((record) => ({ ...record, forwarding: replyForwarder.read(record.inReplyTo ?? record.msgId) ?? record.forwardingError ?? null })), remaining } };
386
549
  } catch (err) {
387
550
  return failure(err);
388
551
  }
@@ -407,10 +570,18 @@ registerTool(
407
570
  try {
408
571
  const session = findClaudeSession(target, { desktopOnly });
409
572
  if (!session) return textResult(`No live Claude session matches "${target}".`, true);
573
+ const transcriptRoot = rootPolicy.enabled ? rootPolicy.capture(session.cwd, "Claude transcript working directory") : null;
574
+ assertSessionRoot(session);
410
575
  if (desktopOnly) {
411
576
  const verified = withDesktopContext(session);
412
577
  if (verified.desktop?.status !== "matched") throw preflightFailure("CLAUDE_DESKTOP_TASK_UNVERIFIED", verified.desktop?.reason ?? "This task is not in the signed-in Claude account.");
413
578
  }
579
+ if (transcriptRoot) {
580
+ const current = findClaudeSession(session.sessionId ?? String(session.pid), { desktopOnly });
581
+ if (!current || current.pid !== session.pid || current.socket !== session.socket) throw new Error("The Claude transcript target changed before it could be read");
582
+ rootPolicy.recheck(transcriptRoot, "Claude transcript working directory");
583
+ if (desktopOnly) assertClaudeSessionProcess(current);
584
+ }
414
585
  const { file, messages } = readTranscript(session.sessionId, session.cwd, limit ?? 10);
415
586
  if (!messages.length) return textResult(`No transcript entries found (looked at ${file}).`);
416
587
  const body = messages.map((m) => `[${m.role}] ${m.text}`).join("\n\n");
@@ -474,7 +645,11 @@ registerTool(
474
645
  const sessions = listClaudeSessions().filter((s) => s.pid !== process.pid);
475
646
  const accounts = readBridgeAccounts();
476
647
  const eligible = sessions.filter((session) => !desktopOnly || session.entrypoint === "claude-desktop")
648
+ .filter((session) => !rootPolicy.enabled || rootPolicy.allows(session.cwd))
477
649
  .filter((session) => !desktopOnly || withDesktopContext(session, accounts.claude).desktop?.status === "matched");
650
+ const visibleInbox = peer.inbox.filter((record) => recordVisible(record, accounts));
651
+ const visiblePending = [...peer.pendingMessages.keys()].filter((id) => readReceipt(id));
652
+ const visibleForwarding = replyForwarder.status((record) => recordVisible(record, accounts));
478
653
  const sender = desktopOnly ? readCodexSenderContext(extra?._meta) : null;
479
654
  const lines = [
480
655
  `platform: ${PLATFORM_LABEL} (${process.platform}/${process.arch})`,
@@ -489,17 +664,17 @@ registerTool(
489
664
  `Claude account: ${accounts.claude.status}${accounts.claude.fingerprint ? ` (${accounts.claude.fingerprint.slice(0, 12)})` : ` - ${accounts.claude.reason}`}`,
490
665
  `Codex account: ${accounts.codex.status}${accounts.codex.fingerprint ? ` (${accounts.codex.fingerprint.slice(0, 12)})` : ` - ${accounts.codex.reason}`}`,
491
666
  `live sessions: ${eligible.length}`,
492
- `excluded: ${sessions.length - eligible.length} non-Desktop or inactive-account session(s)`,
667
+ ...(rootPolicy.enabled ? [] : [`excluded: ${sessions.length - eligible.length} non-Desktop or inactive-account session(s)`]),
493
668
  `relay thread: ${forwarding.threadId ?? "(none - use bind_codex_thread)"}`,
494
669
  `delivery: ${delivery.describe()}`,
495
- `inbox: ${peer.inbox.length} pending message(s)`,
496
- `outstanding: ${peer.pendingMessages.size} message(s) awaiting receipt or reply`,
497
- `reply forwarding: ${JSON.stringify(replyForwarder.status())}`,
498
- ...[...peer.pendingMessages.keys()].map((id) => `pending message: ${id} (${peer.readDelivery(id)?.status ?? "sent_unconfirmed"})`),
670
+ `inbox: ${visibleInbox.length} pending message(s)`,
671
+ `outstanding: ${visiblePending.length} message(s) awaiting receipt or reply`,
672
+ `reply forwarding: ${JSON.stringify(visibleForwarding)}`,
673
+ ...visiblePending.map((id) => `pending message: ${id} (${peer.readDelivery(id)?.status ?? "sent_unconfirmed"})`),
499
674
  ];
500
675
  const state = runtime.status();
501
676
  lines.push(`runtime pid: ${state.pid}`, `loaded source: ${state.revision}`, `disk source: ${state.diskRevision ?? "unreadable"}`, `runtime state: ${state.current ? "current" : `STALE - ${state.reason}; reconnect this MCP server in the existing task`}`);
502
- return { ...textResult(lines.join("\n"), !state.current), structuredContent: { runtime: state, accounts: { claude: publicAccountState(accounts.claude), codex: publicAccountState(accounts.codex) }, replyForwarding: replyForwarder.status(), ...(sender ? { sender } : {}) } };
677
+ return { ...textResult(lines.join("\n"), !state.current), structuredContent: { runtime: state, accounts: { claude: publicAccountState(accounts.claude), codex: publicAccountState(accounts.codex) }, replyForwarding: visibleForwarding, ...(sender ? { sender } : {}) } };
503
678
  } catch (err) {
504
679
  return failure(err);
505
680
  }
@@ -133,22 +133,11 @@ export function readClaudeDesktopContext(session, { platform = process.platform,
133
133
  (typeof session.bridgeSessionId !== "string" || !session.bridgeSessionId.trim())) {
134
134
  return result("mismatch", "The live Claude bridge session identity is invalid.");
135
135
  }
136
- const directory = root ?? (account ? path.join(account.root, "claude-code-sessions") : sessionsRoot(platform, env));
137
136
  let records;
138
137
  try {
138
+ const directory = root ?? (account ? path.join(account.root, "claude-code-sessions") : sessionsRoot(platform, env));
139
139
  if (!fs.existsSync(directory)) return result("missing", "Claude Desktop task metadata is not available on this host.");
140
- const files = metadataFiles(directory, account?.accountId);
141
- const budget = { bytes: 0, versions: new Map(), contents: new Map() };
142
- records = files.map((file) => readMetadata(file, budget));
143
- const currentFiles = metadataFiles(directory, account?.accountId);
144
- if (files.length !== currentFiles.length || files.some((file, index) => file !== currentFiles[index])) {
145
- return result("mismatch", "Claude Desktop task metadata changed during inspection; inspect the existing task again.");
146
- }
147
- for (const file of files) {
148
- const current = fs.lstatSync(file);
149
- if (!current.isFile() || !sameVersion(current, budget.versions.get(file))) throw new Error("metadata changed");
150
- if (!rereadMetadata(file).equals(budget.contents.get(file))) throw new Error("metadata changed");
151
- }
140
+ records = readClaudeDesktopTasks({ platform, env, root, account });
152
141
  } catch {
153
142
  return result("mismatch", "Claude Desktop task metadata could not be read completely and consistently; no task identity is confirmed.");
154
143
  }
@@ -202,3 +191,20 @@ export function readClaudeDesktopContext(session, { platform = process.platform,
202
191
  accountFingerprint: account?.fingerprint,
203
192
  });
204
193
  }
194
+
195
+ export function readClaudeDesktopTasks({ platform = process.platform, env = process.env, root, account } = {}) {
196
+ if (account !== undefined && (account.status !== "verified" || !ACCOUNT_ID.test(account.accountId ?? "") || !account.root)) {
197
+ throw new Error("Claude Desktop account is not verified");
198
+ }
199
+ const directory = root ?? (account ? path.join(account.root, "claude-code-sessions") : sessionsRoot(platform, env));
200
+ const files = metadataFiles(directory, account?.accountId);
201
+ const budget = { bytes: 0, versions: new Map(), contents: new Map() };
202
+ const records = files.map((file) => readMetadata(file, budget));
203
+ const currentFiles = metadataFiles(directory, account?.accountId);
204
+ if (files.length !== currentFiles.length || files.some((file, index) => file !== currentFiles[index])) throw new Error("metadata changed");
205
+ for (const file of files) {
206
+ const current = fs.lstatSync(file);
207
+ if (!current.isFile() || !sameVersion(current, budget.versions.get(file)) || !rereadMetadata(file).equals(budget.contents.get(file))) throw new Error("metadata changed");
208
+ }
209
+ return records;
210
+ }