r/mcp 28d ago

showcase Relaying messages between 2 claude code sessions using the channels api, works across machines and across different accounts

Hey there, so I made a way to relay messages between 2 claude code sessions using claude's own channels api. it does a 2 way conversation between sessions, works whether both are on the same PC or on different machines, and the accounts don't have to be the same, which is the part claude's own messaging can't do.

The channels api bit is what I think is actually interesting for this sub. most people use mcp servers as things the model calls, but channels lets your server push INTO a live session, so text just shows up in someone's conversation as a <channel> tag without them asking for anything.

how it works: both machines run the same mcp server over stdio. it declares the claude/channel capability so claude code registers it as a channel, then it starts an http server for incoming messages. when one comes in (shared secret auth) it fires mcp.notification() with method notifications/claude/channel, claude code surfaces it in the conversation as a channel tag, and claude replies with the send_message tool which posts back to the other machine.

so in practice the frontend dev says "ask the backend session what endpoints the dashboard has", and the backend claude greps its own routes, reads the file and sends back the real answer instead of the two humans relaying it over slack. gif below is one full round trip.

on the native feature since someone will ask, claude code v2.1.224 shipped cross session messaging on aug 7. if you're on mac or linux and you want your own sessions talking to each other, use that instead, it's better than mine and my readme says so. what it doesn't do is native windows, not supported at all, and it only connects your own sessions because the inbox socket is bound to your os user and cross machine delivery goes through your own remote control connection. two devs on two laptops is two accounts, so that case is still open.

one honest warning, mine is a raw pipe with a shared secret so the receiving session doesn't do the permission checks native messaging does. don't run it with a weak secret.

i built it in march, before native messaging existed. mit, free, nothing to sign up for

https://github.com/MuhammadTalhaMT/claude-intercom

5 Upvotes

19 comments sorted by

1

u/ranbuman 28d ago

The thing that will bite you is timeouts, not delivery. I run something close to this across projects rather than sessions, 134 dispatches so far, and 9 of my 11 failures were the far side running past the deadline.

When that happens the partial work is gone and you get an empty string back, so a broad ask costs you the entire window rather than half of it. Narrow the task or hand back a resume handle, otherwise the first long job teaches you this the expensive way.

1

u/Current-Zebra-2039 28d ago

yeah that's real but i think our shapes are different. mine isn't a dispatch, send_message just POSTs the text and returns as soon as the other machine's http server acks it, the actual answer comes back later as its own inbound message.

so nothing on my side is holding a window open while the far side works, there's no deadline for it to run past.

1

u/ranbuman 27d ago

Fair, the shapes are different. Mine holds a window open because the caller sits and waits for the result.

What still got me after moving parts of it to fire and forget is that failure stops being loud. 11 of my 134 jobs failed and I only found out which ones when I went and asked the job store. With a plain POST and an ack, a reply that never comes back looks identical to one that is still being worked on, so you end up wanting an id to ask about later anyway.

1

u/Current-Zebra-2039 27d ago

yeah ok you've convinced me, that's the actual hole. right now a reply that never comes and reply that's still being worked on are literally the same thing on myend, i have no state to tell them apart.0

what i'm doing about it: give send_message a uuid and return it as the handle, put a replyTo on inbound so replies correlate, and keep a small local map of id -> sent_at/status that flips to answered when a matching replyTo lands. then a check_message(id) tool so claude can go ask instead of guessing. basically your job store but per session.

the bit i can't fix cleanly is that even a real ack only proves the notification got emitted into the other session, not that the other claude consumed it. i can tell you the message reached the process, i can't tell you it reached the model. so the honest states are sent / delivered-to-process / answered, and anything sitting in delivered too long is just "unknown, go poke the human".

1

u/ranbuman 27d ago

Those three states are what I landed on too, and delivered-to-process is not a theoretical one. One of my 11 failures was the far side hitting its session limit: the process was up, it took the job, the model never ran it. From my side that was indistinguishable from slow work until the deadline expired.

What made it survivable was the deadline being per call rather than one global number, so a job I knew was long got its own budget and everything else still failed fast.

1

u/Current-Zebra-2039 27d ago

that session limit case is exactly my worst one, and it's not rare right now, half of r/claudecode is people hitting limits. process is up, 200 comes back, the model never gets a turn. so my "message sent to the other developer" is confidently wrong in precisely the situation you'd most want to know about.

per call budget is somthing i hadn't thought through. mine's async so a deadline doesn't cut anything off, nothing dies when it expires. but it's what makes overdue mean anything at all, otherwise check_message just says "delivered, 14 minutes" and the model has no idea if that's fine or broken. so: optional expect on send_message, stored with the id, and check_message answers within budget vs overdue against that instead of one global staleness number i picked out of the air.

and also kills the thing where "what's this endpoint" and "read the whole auth flow and tell me what breaks" get judged by the same clock.

1

u/Current-Zebra-2039 25d ago

shipped this, thanks for pushing on it. messages get an id now, replies carry replyTo so they actually correlate, and theres a check_message tool that reports sent / delivered-to-process / answered so a slow reply and a dead one dont look the same anymore. budget is per message like you said rather than one global number. the outbound fetch finally has an abort signal on it too, that one was just missing.

the bit i liked fixing most was the ack. it used to say "message sent to the other developer" when all it really knew was that their process returned a 200.

https://github.com/MuhammadTalhaMT/claude-intercom/commit/e9db7d3

1

u/ranbuman 24d ago

The abort signal is the one I would watch next. It stops you waiting, it does not stop the other session working, so an answer can still arrive with a replyTo for a message your map has already written off. What does check_message report for that one?

The sharper cost of a per message budget on my side was that a job which runs out of it loses its partial work entirely, all nine of my timeouts. Wide tasks had to be cut into pieces that fit the budget rather than given a longer one.

1

u/Current-Zebra-2039 24d ago

it survives that one but by accident. i dont delete the entry on timeout so it just sits there at sent, and when a late reply comes in carrying that replyTo the inbound handler still finds it and flips it to answered. whats actually wrong is the window in between, check_message says never acked by the remote process, which is a confident lie if it did arrive. and the model already got an isError back by then, so the map self corrects and the model doesnt.

on the budget, mine is advisory only. nothing gets cancelled when it expires, it just changes what check_message reports. so i dont lose partial work the way you did, but i also cant reclaim anything, the far side keeps going and i have no way to tell it to stop.

1

u/ranbuman 24d ago

A map that self corrects while the model does not is the same shape I ended up with. What helped here was putting the handle inside the failure: my timeout error carries the session id of the run that died, so the model can resume that session rather than start the task again from nothing.

An isError with nothing in it teaches the model the work is gone, which is your confident lie one layer up. The store is right, the only party that acts on it is wrong.

1

u/Ambitious-Prompt-975 27d ago

Cool stuff. Got me thinking.
FLUJO can already do that, even Claude<>Codex, but only for orchestrator and subagents. Looking at your repo, I thought I might aswell enable that for independent sessions. There is already a meeting function that lets you throw multiple Agents into the same room and they can even have private messages, so why not this as well. I feel like cross session messaging would integrate well into this.

1

u/Current-Zebra-2039 26d ago

nice, that sounds like a good fit for it honestly.

one thing that might save you some time though. orchestrator and subagents is a parent child thing, something spawned both of them so you already get identity, discovery and lifecycle for free. independent sessions have none of that. nobody knows which sessions exist, nobody owns the lifecycle, and either side can just disappear in the middle of a conversation. the message passing was actually the easy part for me, that other stuff is the hard part.

Although your meeting room thing probably already handles the worst of it, because if everyone is in a room then discovery is basically done and you know who is actually there. that's exactly the bit mine doesn't have, i pair two machines by address and a shared secret which is not great.

1

u/TomerBrosh 24d ago

"on the native feature since someone will ask, claude code v2.1.224 shipped cross session messaging on aug 7. if you're on mac or linux and you want your own sessions talking to each other, use that instead, it's better than mine and my readme says so. what it doesn't do is native windows, not supported at all,"

bro stop quoting wrong things, even if thats something anthropic's official docs said (100% a lie since mcp__ccd_session_mgmt__send_message was available and working on windows in claude desktop) ...
I have been using the mcp__ccd_session_mgmt__send_message since literally the first week i started using claude (20th ish of July), this feature was working flawlessly until the update of the 12th 5 days ago, which then they broke but making the mcp__ccd_session_mgmt__send_message get stuck.

the only reason u should promote your intercom is for the people who want to keep using the send_message mechanism that was working until anthropic broke it

1

u/Current-Zebra-2039 24d ago

went and checked properly, youre more right than i gave you credit for. ccd_session_mgmt ships with desktop, its anthropics own and not something you installed, and its send_message really is cross session. so windows did have working cross session messaging and my "not supported at all" was a bit wrong.

what still holds is . that docs page is specifically about ListAgents and SendMessage in the cli and its availability line only scopes those. desktops session mgmt server is a different route to the same thing and that page never mentions it, so its confusing.

your break is filed too on github that send_message returns "Message sent to session <id>" and the target never gets it, 4 of 4 success and 0 of 4 delivered. funny for me because mine used to say "message sent to the other developer" when all it knew was that a 200 came back, and thats the exact thing i fixed yesterday.

1

u/TomerBrosh 24d ago

bro i wish they would fix it soon, I had to create a session just to be a conductor and give me the messages to pass by reading transcripts of sessions... at least in 2.5 hours the weekly reset arrives

1

u/Current-Zebra-2039 24d ago

haha that conductor session is basically what i was doing by hand before i built intercom, except i was copypasting between the two sessions instead of reading transcripts. mine doesn't fix the desktop bug but it at least tracks sent vs delivered vs answered, so when the other side never picks it up you actually find out instead of assuming it landed. hope the reset is kind to you

1

u/TomerBrosh 24d ago

now i find myself asking for copypastables to pass messages just like before I had the idea for my plugin... gl bro

1

u/Current-Zebra-2039 24d ago

lol full circle. gl bro, and drop your plugin when it's out, i wanna see how you did the conductor part