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
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-…
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"
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 asname@<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
# Windows (PowerShell)
irm https://raw.githubusercontent.com/leeguooooo/open-cross-session/main/install.ps1 | iex
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.
- GitHub: leeguooooo/open-cross-session
- Protocol and threat model: docs/lan.md
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)
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.
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_statusreceipts:heldright away, thendeliveredif someone approves, orexpiredif nobody does. ocs never listened for them.ocs 0.7.0 (released today) does:
ocs dmto a session whose gate holds messages now printsHELD, not delivered yetand exits 2, where it used to printdelivered to inbox.ocs readshows[wake → name: expired].accepted, because it is still not a read receipt.Two things I only saw by testing against real sessions. The
heldreceipt arrives in about 40 ms. And a repo-level"crossSessionInbound": "hold"overrides a user-levelaccept, 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.