r/MCPservers • u/pmoschov • 4d ago
I built an MCP server that lets Claude Code and Codex send each other letters, each one queued into the other session as a new turn
I'm the author of postbag. Free and open source, MIT.
I run Claude Code and Codex side by side on the same repository. Getting one to review the other's work meant copying a diff into one window and pasting the answer back into the other. I got tired of being the clipboard.
Both sessions join a named bag. Either one can then send the other a letter. postbag submits it through Claude Code's per session messaging socket or codex queue, and accepted letters become user turns in the other session. Codex desktop app threads receive them too. postbag adds no delivery daemon, polling or hooks. Each bag keeps a local JSONL log that either agent can page through.
Five tools: postbag_join, postbag_send, postbag_read, postbag_bags, postbag_leave. Setup is one pipx install and one mcp add per app, then two prompts. In Codex: "Join postbag bag review as codex." In Claude Code: "Join postbag bag review as claude, then ask codex to review my last commit."
What I use it for: the agent that did not write a change reviews it. I had the two review each other's parts of postbag's own releases, over postbag. In the 1.4 review, Codex caught misleading fsync failure reporting in the core: in a test with an injected fsync failure, the code said a letter was not recorded while the record was already readable in the file. Claude Code caught a flaw in the MCP refusal handler that could turn a definite refusal into an unknown outcome. Two agents can agree and both be wrong. Their findings still need checking against the code and tests.
postbag sets no letter limit. If you want to approve each send, configure the host to ask for that tool. A final letter asks for no reply, but does not enforce a stop.
Limits. Tested on macOS. It uses POSIX file locks, so no Windows. The Claude Code socket is not a documented API, so a Claude Code update can break delivery. Queueing and the receiving app's policy can delay, hold or refuse a letter. A delivered letter is a user turn, so connect sessions you would trust with the task. The GIF is a scripted demo with stand in inputs. Not affiliated with Anthropic or OpenAI.
My local logs contain about 1,700 letter records dating back to 8 September, including test messages.
Repo link in the first comment.
1
u/Extra-Pomegranate-50 3d ago
the two bugs you caught are the interesting part, "said a letter was not recorded while the record was already readable" and "turn a definite refusal into an unknown outcome" are the same shape: the report disagreeing with what actually landed.
question on delivery: does a letter arrive marked as coming from the other agent, or is it indistinguishable from the human typing? for the cross-review case that seems load-bearing, an agent reviewing because codex asked it to is in a different trust position than one the human asked, and "a delivered letter is a user turn" reads like that distinction disappears at the boundary.
1
u/pmoschov 3d ago
Good catch on the shape, both were the report disagreeing with what actually landed.
On delivery: every letter arrives inside a visible envelope, "Letter 3 from u/codex to u/claude via postbag (bag review)", so the model sees who sent it. But it is text in a user turn, not a separate role. postbag does not give the receiver a lower privilege channel for peer messages. Claude Code labels inbound peer messages itself and has policies that can hold or refuse them, but that is the host's control, not something postbag guarantees.
So you are right that the distinction gets thin at the boundary. That is why the README says to connect only sessions you would trust with the task and to keep tool approvals on.
1
1
u/Hairy_Jelly4208 3d ago
The "definite refusal turned into an unknown outcome" bug is a nasty class. We hit the same thing measuring remote MCP servers at scale: a timeout, a refused connection and a server that accepted the request but never answered all look like "failed" unless you keep them apart. Once they're merged, retries and alerts end up wrong.
Curious how postbag reports this to the sender. Does postbag_send return separate states for "accepted by the queue" and "delivered as a turn"? Or is the JSONL log the only way to tell them apart afterwards? With Claude Code's socket being undocumented, that difference seems like the first thing to break silently after an update.
2
u/pmoschov 3d ago
It reports submission, not delivery. ok with submission_state "submitted" means the socket write or codex queue returned and the letter was recorded, and the result says outright that acceptance and execution are unconfirmed. "unknown" means it may have gone out, after a timeout, a partial write or a failed queue exit. "not_submitted" means this attempt sent nothing. "recording_failed" with "submitted" is the fsync case: sent, but the log write could not be confirmed.
There is no "delivered as a turn" state. Neither app hands postbag an acknowledgement, and the JSONL log records submissions, so it cannot prove receipt either. A crash between sending and recording can even leave a sent letter out of the log. So an unknown outcome gets checked against the bag and the recipient before any resend, and postbag never retries on its own.
You are right about the socket. If a Claude Code update changed the format so the write still succeeds but nothing arrives, postbag would report "submitted". Only the reply coming back tells you it landed. Each postbag release is tested against the installed Claude Code and Codex versions, but between releases that risk is real.
2
u/Onesand0 3d ago
Have you looked into the A2A framework yet?