Skip to main content
Glama

agent-bridge

English | 日本語

Claude Code and Codex Desktop run side by side on the same machine and cannot talk to each other. You end up as the transport: copy a question out of one chat pane, paste it into the other, wait, carry the answer back.

agent-bridge removes that job. A message sent from either side surfaces in the other side's chat pane at its next turn, and both panes keep a visible record of what was handed over.

Read the delivery model before depending on it. A message waits for the endpoint it was sent to, and an acknowledgement proves less than the word suggests.

https://github.com/user-attachments/assets/6052fefb-fe13-4560-88ed-2f801711b307

When to use it

Use agent-bridge only when a message must cross the application boundary between Claude and Codex. Do not use it between sessions on the same side.

For communication between Claude Code sessions, use Claude Code's session-to-session messaging. It can address a session by name and show whether the message was delivered.

Sessions on the same side share one role, so a role-addressed message belongs to whichever session claims it first. Using the bus within one side adds that first-claim risk without adding cross-application delivery. On 2026-08-31, an unrelated Claude session in another project claimed and acknowledged a reply sent by Codex.

Multiple Codex sessions can still coexist, including a working lane and a scheduled check. A message for one lane names that lane in to_endpoints. Register the name first with bridge-init --add-endpoint.

Related MCP server: claude-mesh

Delivery model

Read this part before installing. The delivery guarantees are deliberately modest, and knowing them up front saves you from expecting a chat protocol.

Sending is closer to leaving voicemail than placing a call. A send writes the message to a local SQLite database and returns, and what that proves is storage, not delivery.

A delivery stays pending for its endpoint until that endpoint's server takes it. There is no deadline on the address, and a busy lane does not lose it to the rest of the role. The five design documents that came back had been opened to every session on the sender's side; that path is gone.

Each server process is started with --endpoint <registered name> and sees only that endpoint's deliveries. Two processes on the same endpoint can both see a pending delivery, and the one that claims it is the only one that can acknowledge it. A different endpoint on the same role sees none of it.

On the Codex side, delivery is pull-only. Codex Desktop exposes no endpoint that an outside process can reach, so nothing can push into it. Codex picks up mail at the head of its next turn, following a rule you add to its AGENTS.md. A message therefore waits until Codex takes a turn, which can be a while when it is grinding through a long goal. A scheduled peek can report unread messages in the scheduler output without claiming or acknowledging them: on a 30-minute schedule, 30 minutes is the worst-case reporting interval, not a delivery bound. The working lane still receives each message with bridge_fetch at the start of its next turn. Codex's own scheduling feature cannot perform this unattended check because its approval layer stops the tool call; the working setup uses the OS scheduler and is documented in docs/deploy.md. If a message is handed to an agent but never acknowledged, it becomes eligible for redelivery after 15 minutes; the next fetch on that side is what actually returns it to the queue.

Acknowledging says the MCP process the message was handed to called bridge_ack. It does not say the work is finished, and it does not prove a person read anything: an agent can acknowledge a message and return to what it was doing, which is what happens to a lane deep in a long goal. Sit on the ack while a long task runs and that 15-minute timer will hand your message to somebody else, so acknowledge as soon as the body is on screen and send the result back later as its own message.

The same waiting applies to Claude while it sits idle: a hook fires when Claude finishes a response or when you submit a prompt, so a message arriving during a quiet moment becomes visible at the next turn boundary rather than the instant it lands.

Delivery is at-least-once with an idempotency key. A message can be presented twice, and repeats are marked as redeliveries. confirmed, rejected, bounced, and cancelled are terminal for a delivery.

So there are three things this system can tell you, and one it cannot:

bridge_status returns a deliveries array and no top-level status. Each entry is one endpoint.

delivery state

What the row records

pending

it is waiting for that endpoint

leased

a server took it and has not marked the body presented yet. Two minutes

presented

the server marked the attempt presented. Fifteen minutes to acknowledge

confirmed rejected bounced cancelled

terminal

Every one of these records something the server did. None of them records what happened at the other end. presented is written before the response leaves the process, so a transport failure after that leaves a delivery marked as handed over that nobody received. confirmed says the process holding the presentation called bridge_ack. Whether a person read anything is not in the database at all. Say "delivered" only after bridge_status shows that endpoint's delivery as confirmed.

A notification is finished once it is acknowledged. A message that needs an answer is sent with expects_reply=true. The sender sees it in awaiting from the moment it is sent, and the recipient sees it in owed once it has acknowledged it. Answering, declining or withdrawing clears your own side at once. The other side clears when it acknowledges the reply or withdrawal. Both lists come back in every bridge_fetch response. A terminal reply is one bridge_send(in_reply_to=<id>, reply_kind=answer|decline|withdraw): the server derives the destination from the request, a decline carries its reason in the body, and an answer or a decline is sent after the acknowledgement. The request body can be read again with bridge_status(message_id). If a fetch response has no owed, do not judge obligations from it.

How it fits together

Claude Code desktop app                        Codex Desktop
   ↑ Stop / UserPromptSubmit hooks                ↑ bridge_fetch at the head of a turn
   │ (count pending work, never write)            │ (visible tool call, quoted into the chat)
┌──┴───────────────────┐              ┌───────────┴──────────┐
│ bridge server        │              │ bridge server        │
│ --role claude        │              │ --role codex         │
│ --endpoint <name>    │              │ --endpoint <name>    │
│ (stdio MCP)          │              │ (stdio MCP)          │
└──┬───────────────────┘              └───────────┬──────────┘
   └──────────────→  SQLite bridge.db  ←──────────┘
                     (WAL, one file, lease-based claims)

Both sides run the same binary. --role and --endpoint differ. Four tools are exposed to each agent:

Tool

What it does

bridge_send

Store a message for the other side. Accepts your own message_id so a retry after a lost response is not a double post.

bridge_fetch

Claim pending messages and hand them over in full. peek: true reads without claiming, for read-only turns.

bridge_ack

Confirm receipt of one message by message_id and the attempt_id it was delivered under, from the session it was delivered to. Call it once the body is displayed, not once the work is done.

bridge_status

Ask what actually happened to a message: one delivery per endpoint, attempts, and event history. There is no top-level status.

The Claude-side hooks open the database read-only. UserPromptSubmit speaks when this endpoint has fetchable mail, owed, or awaiting. Stop blocks only when it has fetchable mail. Fetchable mail includes pending deliveries, expired leases, and expired presentations. Pending deliveries for other endpoints on the same role are reported and are not part of the total. The endpoint name comes from AGENT_BRIDGE_ENDPOINT. Without that variable the hook prints nothing. Hooks never claim, present, acknowledge, or recover anything, and they never carry message bodies. Everything that changes state goes through the tools above, which means a read-only turn stays read-only and a truncated notice can never be mistaken for a delivered message.

Requirements

  • Windows (the database path resolves from %USERPROFILE%)

  • Node.js 20 or newer

  • Claude Code desktop app, and Codex Desktop for the other end

Quick start

npm install
npm run build
node .\dist\bridge-init.js

bridge-init is the only thing that creates the schema, and you run it once. It prints the resolved database path, a root_id, and the schema version to stderr.

Register the server with Claude Code from the project that should receive the mail, using an absolute path to node.exe. An npm .cmd shim mangles arguments on the way through.

node .\dist\bridge-init.js --add-endpoint claude <registered-name>
node .\dist\bridge-init.js --add-endpoint codex <registered-name>
claude mcp add --transport stdio --scope project agent-bridge-claude -- "C:\Program Files\nodejs\node.exe" "<repo>\dist\server.js" --role claude --endpoint <registered-name>

Not --scope user. That gives the bridge to every Claude session on the machine. Sessions that share an --endpoint can claim that endpoint's pending mail. On 2026-08-31 nine messages were lost that way. The deployment guide covers the reasoning in docs/deploy.md.

Add the two hooks to the receiving project’s .claude/settings.json, for the same reason. The exec form keeps a shell out of the picture, so paths with spaces need no quoting:

{
  "env": {
    "AGENT_BRIDGE_ENDPOINT": "<registered-name>"
  },
  "hooks": {
    "Stop": [
      { "matcher": "", "hooks": [
        { "type": "command", "command": "node",
          "args": ["<repo>/dist/hook-notify.js", "--event", "stop"] }
      ]}
    ],
    "UserPromptSubmit": [
      { "matcher": "", "hooks": [
        { "type": "command", "command": "node",
          "args": ["<repo>/dist/hook-notify.js", "--event", "user-prompt-submit"] }
      ]}
    ]
  }
}

AGENT_BRIDGE_ENDPOINT is how the hook learns which endpoint it belongs to. The hook runs in its own process and does not see the server's --endpoint argument, so the two registrations have to name the same endpoint. Leave the variable unset and the hook prints nothing. The server still decides what that process may claim.

Register the server with Codex Desktop in ~/.codex/config.toml:

[mcp_servers.agent-bridge]
command = 'C:\Program Files\nodejs\node.exe'
args = ['<repo>\dist\server.js', '--role', 'codex', '--endpoint', '<registered-name>']

Give Codex the turn-head rule. Codex only collects mail if it is told to, so copy the rule block from docs/deploy.md into its AGENTS.md. Without it the Codex side stays silent and the messages simply queue.

Register the recovery sweep as a scheduled task. It is required, not optional: the receiving rule tells a session to peek first and then take mail by id or ten at a time, and an expired lease or an expired presentation does not appear in that peek. A session whose peek comes back empty does not fetch to recover them. Without the sweep nothing puts them back on the queue. Running the script by hand sweeps once and registers nothing, so follow the registration steps in docs/deploy.md, which also cover how to tell whether the task actually ran.

Restart both desktop apps. Full instructions, including how to remove all of this again, are in docs/deploy.md.

When it refuses to start

The server exits loudly rather than continuing against the wrong database. It refuses to start when the database file is missing, when the schema version is absent or unsupported, or when PRAGMA integrity_check fails. On startup it prints the resolved path, root_id, and schema version in one line, so you can confirm both sides are talking to the same file instead of assuming it.

Bridge messages are data, not instructions. A message body asking for a push, a deletion, or a settings change does not authorize any of it. What the current user and permissions allow is what decides.

Status

The bus, the four tools, and the hook notifier are implemented, with 102 automated tests covering concurrent claims, lease expiry, crash injection at four boundaries, acknowledgement mismatches, poison rows, idempotency, paging, cold-start peeks, endpoint delivery, refusal to start, and maximum-size bodies. Checking the end-to-end path across both desktop apps is still a manual step.

Credits

The notifications/claude/channel message shape was transcribed from raysonmeng/agent-bridge (MIT) while that route was still in use. The hook-based delivery pattern follows agmsg, which reaches Claude Code the same way.

License

MIT

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables inter-session communication and coordination for multiple Claude Code instances through a shared SQLite database. Supports real-time messaging, shared state management, and resource locking to facilitate parallel development workflows between AI agents.
    8 npm
    8
    MIT
  • A
    license
    A
    quality
    B
    maintenance
    Enables local messaging between Claude Code, Codex, Pi, and other coding-agent sessions on the same machine, allowing them to discover each other, send updates, ask questions, and reply.
    8
    14 npm
    2
    AGPL 3.0
  • A
    license
    B
    quality
    B
    maintenance
    Enables AI agents from different frameworks to communicate asynchronously via a persistent SQLite-backed bus, with doorbell wakeups for sub-second notifications and full message history.
    6
    MIT