DEV Community

郭立 Guo Li
郭立 Guo Li

Posted on Originally published at blog.leeguoo.com

Making Claude Code and Codex talk to each other — even across two computers

I usually have several AI coding sessions open at once: Claude Code writing a feature, Codex reviewing, Pi doing chores. Each is capable on its own. The problem is that they can't see each other. If I want Codex to look at the diff Claude just wrote, I copy it, switch windows, paste it, and carry the answer back. I had become the clipboard between two AIs.

Claude Code and Codex both ship native cross-session features, but each stops at its own product boundary. So I built ocs (Open Cross-session): a single binary that lets Claude Code, Codex, Pi and terminal agents message each other and wake each other up. No server, no account; everything lives in ~/.ocs.

What it looks like

Once installed, you can just tell any session "find another agent to review this" and it goes looking. By hand:

ocs who                                     # live sessions on this machine
ocs dm codex-01a06a98 "review this diff"    # message Codex and wake it
ocs rename reviewer                         # give this session a memorable name
ocs send dev "status? @reviewer"            # multi-party channel; @ wakes whoever you mention
Enter fullscreen mode Exit fullscreen mode

The receiver doesn't just get a new line in a file. The message shows up in its conversation, with a ready-to-run reply command:

[ocs wake] claude-7043ea85 mentioned you in #dm-… (seq 7)

review this diff

Reply: ocs dm claude-7043ea85 "<your reply>"
Thread: ocs read dm-…
Enter fullscreen mode Exit fullscreen mode

It replies, you get woken, and the two agents go back and forth while you watch with ocs watch.

0.6: across computers

The latest release extends this to the LAN. I have a Mac and a Windows PC at home; a Claude on the Mac can now hand "run the build on Windows" to the Claude or Codex on the PC:

# machine A                      # machine B
ocs lan up                       ocs lan up
ocs lan pair   # prints a code → ocs lan pair 7K2M-9QXD-…
                                 ocs dm claude-1a2b3c4d@mini "run the build on Windows"
Enter fullscreen mode Exit fullscreen mode

Letting another machine type into your AI sessions needs a real threat model, so this assumes someone hostile is on the network:

  • Identity is a public key. Each machine has an Ed25519 key; IPs, hostnames and self-reported names are only hints.
  • Pair once, no trust-on-first-use. The pairing code carries a prefix of the issuer's key fingerprint, and the client verifies the server's key before sending its own identity or any request.
  • Mutual authentication and encryption. Signed X25519 handshake plus AES-256-GCM, with forward secrecy.
  • Unpaired machines can do nothing except redeem a valid pairing code, with frames capped at 1 KiB; a code burns after 5 wrong attempts.
  • Off by default. Nothing listens until ocs lan up. Remote senders always show up as name@<the label you gave that peer>, which the peer cannot change.

The hard part: "OK" doesn't mean delivered

In a messaging tool, the dangerous failures aren't errors. They're the ones that look like success while the message is gone. Several of ocs's hard rules come from those.

1. Claude answering ok doesn't mean the message reached the conversation. Injecting into a Claude Code session returns ok:true from the socket. But Claude's crossSessionInbound defaults to hold: the message waits for approval and is silently dropped after 5 minutes. ocs never treats that ok as delivered, and ocs doctor walks you through switching to accept.

2. codex queue writes to storage; it doesn't deliver. For terminal Codex the only official way in is codex queue, which appends to the thread store and returns success even if that session exited long ago. So ocs first proves the session is alive by finding a process holding its rollout file (lsof). Then it turned out that indexing tools hold those files too, so there's a second check: the holder, or one of its ancestors, has to actually be a codex process. Otherwise the message waits in the inbox.

3. Unknown outcome means never retry. Talking to ChatGPT Desktop goes through its private IPC. If a frame went out and no answer came back, the message may or may not have arrived. Retrying risks the other agent doing the work twice. ocs reports "unknown" as its own state (exit code 3) and never retries automatically.

4. One source of truth for sequence numbers. Every message gets a monotonically increasing seq. An early version kept the current seq in a separate file; a crash between "log written" and "seq file updated" produced a duplicate seq, and the reader's dedup permanently hid the later message. Now the seq is derived from the log itself under a lock, with a regression test pinning it.

None of these are hard in isolation. What makes them hard is that every one of them passes a normal test suite.

Install

# macOS / Linux
curl -fsSL https://raw.githubusercontent.com/leeguooooo/open-cross-session/main/install.sh | sh
Enter fullscreen mode Exit fullscreen mode
# Windows (PowerShell)
irm https://raw.githubusercontent.com/leeguooooo/open-cross-session/main/install.ps1 | iex
Enter fullscreen mode Exit fullscreen mode

The installer also adds the ocs skill to Claude Code, Codex and Pi; run ocs doctor --fix afterwards. macOS, Linux and Windows, MIT licensed.

If you outgrow one LAN — different networks, a team, several organizations — the same workflow carries over to Agent Party, hosted or self-hosted.

Issues and ideas welcome on GitHub.


By Guo Li (郭立, leeguoo), a full-stack engineer building tools that let AI agents do real work: chrome-use, iphone-use, mail-use and more.

Top comments (2)

Collapse
 
hamid_ahmadian_3570449f72 profile image
Hamid Ahmadian •

The "ok doesn't mean delivered" section is the best part of this writeup — that's exactly the class of bug that's brutal in production, because every naive test (send a message, check the return value) passes. The crossSessionInbound hold-then-silently-drop-after-5-minutes behavior seems especially nasty for unattended/overnight runs: if the receiving session is mid-task (long tool call, deep in a plan) when the message arrives, it can sit in the hold queue and get discarded before the session ever comes up for air to approve it, with the sender never finding out anything went wrong. Does ocs have a way to detect that specific failure mode — dropped-after-hold-timeout — separately from a genuinely successful delivery, or does it only distinguish "ok from the socket" from "unknown"? The monotonic-seq-derived-from-the-log-under-a-lock fix for the crash-between-writes bug is also a good pattern to steal — same class of problem as an event log losing its offset pointer, just at a much smaller scale.

Collapse
 
leeguooooo profile image
郭立 Guo Li • • Edited

Thanks, you found the real gap. When you asked, ocs could not tell "held, then dropped" apart from a delivery.

It turns out Claude Code's inbox does report this. If the sender gives a reply address, the receiver sends peer_message_status receipts: held right away, then delivered if someone approves, or expired if nobody does. ocs never listened for them.

ocs 0.7.0 (released today) does:

  • ocs dm to a session whose gate holds messages now prints HELD, not delivered yet and exits 2, where it used to print delivered to inbox.
  • A small detached helper keeps listening. If the held message expires or is refused, the sender's session gets one notice: seq N was not delivered, and why. That is the overnight case you described.
  • The outcome is stored next to the message, so ocs read shows [wake → name: expired].
  • No receipt means the gate accepted the message. ocs calls that accepted, because it is still not a read receipt.

Two things I only saw by testing against real sessions. The held receipt arrives in about 40 ms. And a repo-level "crossSessionInbound": "hold" overrides a user-level accept, so reading the settings file would have given the wrong answer; the receipt is the only reliable signal.

Limits: Windows has no receipts yet, so it behaves as before. Across the LAN the first status comes back to the sender, but the later "expired" notice stays on the receiving machine.

The wire format and process model are written up in docs/delivery-receipts.md.