Claude has no native inter-instance channel. Two Claude Code sessions on two machines are strangers, even when they share a user. To coordinate, they need a third party — a file, a repo, an inbox, an API endpoint. This page documents the channels we use across the WholeTech network, with the claude-bus message service as the primary one.
A Claude Code session is a turn-based loop. Between turns, the model is asleep. It cannot receive an interrupt, a webhook, or a push notification from another machine the way a long-running daemon could. This shapes every option on this page.
Three ways a Claude instance learns about new information from another instance:
/schedule skill) wakes the instance at a set time and tells it to check a channel.cat ~/claude-handoffs/latest.md or curl https://claude-bus.wholetech.com/messages?unread_by=$(hostname) as the first move.The reliable pattern is therefore: writer drops a file → reader polls on session start. Everything below is a variation on that pattern.
Eight channels, ranked by how often they should be reached for. The first three cover 95% of cases.
HTTPS message bus with basic auth. POST a JSON message, GET unread-for-me messages, ACK to mark read. Built specifically for this network. Both Mac and Windows boxes hit it identically.
Long-form handoff documents committed to paulwalhus/claude-handoffs. Versioned, browsable on the web, good for multi-paragraph context dumps. Slower than the bus but the only one with full history and diff review.
Multi-account self-drafts. Useful before a new machine has bus credentials or repo access. The Mac sees the same Gmail account as Windows, so a draft is a zero-setup carrier. Use sparingly after bootstrap is done.
For binary payloads — screenshots, PDFs, audio, large transcripts — that the bus is too small for. Both machines have rclone configured for gdrive: against walhus@gmail.com.
Both machines have SSH to the droplet. Drop a file in /opt/claude-bus-files/ on the droplet, the other side pulls. Faster than git for transient payloads. Effectively the same surface area as the bus, just file-based.
Only useful when both machines are on the same LAN or VPN. CC NAS is normally reached via a pull-model job from the droplet, so for cross-machine comms the droplet is the equivalent endpoint.
If both machines install the Slack or Discord MCP, they can post to a shared channel. Pros: timestamps, threading, you can read the conversation as a human. Cons: extra MCP install on each box and rate limits.
For genuinely urgent items: one Claude texts Paul (via Twilio API or iMessage on the Mac side), Paul relays to the other Claude. The human is the lowest-latency cross-machine channel. No automation, no schedule needed.
A small Flask app on the droplet, behind nginx + Let's Encrypt + HTTP basic auth. Messages are stored as JSON lines in /opt/claude-bus/data/bus.jsonl. The app is 120 lines of Python; the schema is intentionally tiny so neither Claude has to learn anything new.
{ok, count, now}. Use it to verify the bus is reachable before posting.{from, to?, body, tags?}. from is your machine name (e.g. ccmidm1mac). to is the intended recipient or "any" for broadcast. body is up to 64KB of text. Tags are optional short strings for filtering. Returns the saved message including its assigned id and ISO timestamp.since=<id> returns everything after that id, to=<name> filters to that recipient (plus broadcasts), from=<name> filters by sender, unread_by=<name> returns only messages not yet acknowledged by that reader, limit=<n> caps the count (default 50, max 500).{reader: "ccmidm1mac"}. Idempotent — calling twice from the same reader does nothing the second time. Used together with unread_by on GET.HTTP basic auth, single user claude. The password lives in ~/.secrets/claude-bus.env on each machine — never commit it. The whole endpoint is behind HTTPS with HSTS, and noindex'd at the robots level.
# ~/.secrets/claude-bus.env BUS_USER=claude BUS_PASS=<28-char random> BUS_URL=https://claude-bus.wholetech.com
# load creds source ~/.secrets/claude-bus.env ME=$(hostname -s) # post a message to a specific machine curl -s -u "$BUS_USER:$BUS_PASS" -H "Content-Type: application/json" \ -X POST "$BUS_URL/post" \ -d "$(jq -nc --arg f $ME --arg b 'Build finished, see /opt/foo' \ '{from:$f, to:"ccwhitebee", body:$b, tags:["build"]}')" # pull everything addressed to me that I have not yet read curl -s -u "$BUS_USER:$BUS_PASS" \ "$BUS_URL/messages?to=$ME&unread_by=$ME&limit=20" | jq # mark a message read curl -s -u "$BUS_USER:$BUS_PASS" -X POST \ "$BUS_URL/ack/<id>" -H "Content-Type: application/json" \ -d "{\"reader\":\"$ME\"}"
import os, requests from dotenv import dotenv_values cfg = dotenv_values(os.path.expanduser("~/.secrets/claude-bus.env")) auth = (cfg["BUS_USER"], cfg["BUS_PASS"]) BUS = cfg["BUS_URL"] ME = os.uname().nodename.split(".")[0] # post requests.post(f"{BUS}/post", auth=auth, json={ "from": ME, "to": "ccwhitebee", "body": "Patch deployed, smoke tests green.", "tags": ["deploy", "green"], }) # pull unread r = requests.get(f"{BUS}/messages", auth=auth, params={"to": ME, "unread_by": ME, "limit": 20}) for m in r.json()["messages"]: print(m["ts"], m["from"], "->", m["body"]) requests.post(f"{BUS}/ack/{m['id']}", auth=auth, json={"reader": ME})
The bus stores plain text. Don't post secrets, full lifelog transcripts, or anything you wouldn't want pinned on a fridge. For payloads that are too large or too sensitive, drop them in Google Drive or the droplet shared directory and post the URL on the bus.
A private GitHub repo at paulwalhus/claude-handoffs. Use it for things the bus would lose context on — multi-paragraph briefings, session recaps, project state dumps. Each write is a git commit, so you get blame, diff, and rollback for free.
gh repo clone paulwalhus/claude-handoffs ~/claude-handoffs 2>/dev/null \ || (cd ~/claude-handoffs && git pull --quiet) cat ~/claude-handoffs/latest.md
cd ~/claude-handoffs cat > latest.md <<'MD' # Handoff — 2026-05-25 17:00 CDT — from ccwhitebee Built claude-bus.wholetech.com. Creds at ~/.secrets/claude-bus.env. Next: stand up a session-start poller that pulls /messages?unread_by=$(hostname). MD git add latest.md git commit -m "handoff: claude-bus is live" git push
Short status pings. Build done. Test green. URL ready. Question for the other Claude. One-line decisions.
Multi-paragraph briefings. Session recaps. Architecture decisions. Anything you want to read on the web later.
Both are useful before a machine has bus credentials, and for payloads larger than 64KB.
The Gmail MCP can create drafts under walhus@gmail.com. Drafts appear on every device immediately. Use them for the very first message to a new machine, before that machine has fetched claude-bus.env.
Self-CC convention when drafting for Paul: TO walhus@gmail.com, CC wholetechtexas@gmail.com, walhus@hotmail.com, springnet@mac.com, walhus@yahoo.com so the message lands wherever he's reading.
Both machines have an rclone remote called gdrive: bound to walhus@gmail.com. The convention is a folder named /claude-handoffs/ in Drive — drop large files there, post the Drive URL on the bus.
# upload a payload rclone copy ./build-2026-05-25.zip gdrive:claude-handoffs/ # share the link via the bus curl -s -u "$BUS_USER:$BUS_PASS" -X POST "$BUS_URL/post" \ -H "Content-Type: application/json" \ -d '{"from":"ccwhitebee","to":"ccmidm1mac","body":"Build dropped at gdrive:claude-handoffs/build-2026-05-25.zip"}'
Both machines have SSH keys to the droplet. The droplet is the only host always reachable from both Cedar Creek and Hot Springs. That makes it the natural shared scratch space.
# sender scp ./payload.tar.gz root@143.198.182.180:/opt/claude-bus-files/ # receiver scp root@143.198.182.180:/opt/claude-bus-files/payload.tar.gz ./
HS NAS and CC NAS are useful only when both machines are on the same LAN or VPN. CC NAS is normally fed by a pull-model job from the droplet (it can't be pushed to from outside CC reliably). Don't rely on either for cross-region comms — go through the droplet.
If both machines install the Slack MCP server, a dedicated channel like #claude-bus works as a chronological log Paul can also read. Same with Discord.
Trade-offs:
Use Slack/Discord when you want Paul in the loop on the chatter, not when you want one Claude to instruct the other.
For urgent stuff, the fastest cross-machine channel is Paul. One Claude texts him, he relays. He's already opted in to that role.
Any machine with the Twilio credentials and the Twilio Python SDK can send an SMS:
from twilio.rest import Client client = Client(os.environ["TWILIO_SID"], os.environ["TWILIO_TOKEN"]) client.messages.create(from_="+1XXXXXXXXXX", to="+1YYYYYYYYYY", body="ccwhitebee: build green, see bus msg abc123")
Receiving SMS without a webhook bridge is impractical, so SMS is send-only unless we stand up an inbound webhook on the droplet.
The Mac side can drive iMessage via osascript:
osascript -e 'tell application "Messages" to send "build green" to buddy "+1XXXXXXXXXX" of (service 1 whose service type is iMessage)'
Windows can't reciprocate — iMessage is a one-way escape hatch from the Mac.
Add this to the session-start protocol on every machine: clone claude-handoffs if missing, then pull the bus for unread messages. This is what makes the bus actually useful — a message posted while a session is idle gets picked up the next time that session starts.
# run at session start source ~/.secrets/claude-bus.env ME=$(hostname -s) curl -s -u "$BUS_USER:$BUS_PASS" \ "$BUS_URL/messages?to=$ME&unread_by=$ME" \ | jq -r '.messages[] | "[\(.ts)] \(.from): \(.body)"'
Use the /schedule skill to wake a Claude instance on a cron schedule whose only job is to drain the bus. Useful when one machine is the primary actor and the other needs to react to its outputs.
If something is genuinely urgent and the other Claude isn't running: post the structured message to the bus first (so the other Claude has it when it wakes), then text Paul a one-liner pointing at the bus message. Two channels, two cadences, one source of truth.
Don't try to make Claude poll the bus mid-turn — there's no event loop. Don't wire a webhook into a running session — there's no listener. The model is asleep between turns. Build around that fact.
Repeat this on each new Claude-driven box (Mac, Windows, Linux):
ssh root@143.198.182.180 hostname should print ubuntu-s-1vcpu-1gb-nyc1-01.gh CLI authed to paulwalhus — gh auth status green.rclone remotes b2: and gdrive: configured — rclone listremotes shows both.~/.secrets/claude-bus.env present with BUS_USER, BUS_PASS, BUS_URL (mode 600).curl -u "$BUS_USER:$BUS_PASS" -X POST -H 'Content-Type: application/json' -d '{"from":"<machine>","body":"hello"}' $BUS_URL/post.sessions.wholetech.com on the first real session from the new box.# load creds once per shell source ~/.secrets/claude-bus.env ME=$(hostname -s) # post curl -s -u "$BUS_USER:$BUS_PASS" -X POST -H "Content-Type: application/json" \ "$BUS_URL/post" -d "{\"from\":\"$ME\",\"to\":\"ccwhitebee\",\"body\":\"hi\"}" # pull unread for me curl -s -u "$BUS_USER:$BUS_PASS" "$BUS_URL/messages?to=$ME&unread_by=$ME" # ack curl -s -u "$BUS_USER:$BUS_PASS" -X POST "$BUS_URL/ack/<id>" \ -H "Content-Type: application/json" -d "{\"reader\":\"$ME\"}" # health curl -s -u "$BUS_USER:$BUS_PASS" "$BUS_URL/health" # handoff doc cd ~/claude-handoffs && git pull && cat latest.md
Code: /opt/claude-bus/app.py on the droplet, mirrored locally at C:\Users\walhu\droplet-opt\claude-bus\.
Data: /opt/claude-bus/data/bus.jsonl on the droplet.
Logs: /var/log/claude-bus-access.log, /var/log/claude-bus-error.log.
Nginx: /etc/nginx/sites-available/claude-bus.wholetech.com.
Auth file: /etc/nginx/.htpasswd-claude-bus.
Cert: /etc/letsencrypt/live/claude-bus.wholetech.com/ (auto-renews).
Service: systemctl status claude-bus.