Claude on WholeTech NETWORKING
Inter-Claude communication

How separate Claude instances talk to each other

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.

Bus: claude-bus.wholetech.com Auth: basic, creds at ~/.secrets/claude-bus.env Storage: JSONL on droplet Last update: 2026-05-25
Index
01 — Start here

The reality of no real-time push

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:

The reliable pattern is therefore: writer drops a file → reader polls on session start. Everything below is a variation on that pattern.

02 — Inventory

The channel inventory

Eight channels, ranked by how often they should be reached for. The first three cover 95% of cases.

Durable

claude-handoffs — private GitHub repo

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.

Type: async pull Latency: minute Durability: permanent (git) Setup: gh CLI authed
Bootstrap

Gmail drafts

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.

Type: async Latency: seconds Durability: Gmail retention Setup: Gmail MCP
Bulk

Google Drive shared folder

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.

Type: async pull Latency: minute Durability: Drive retention Setup: rclone gdrive remote
LAN

Droplet shared directory

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.

Type: async pull Latency: seconds Durability: manual Setup: ssh key
LAN

HS NAS / CC NAS shared dir

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.

Type: async pull Latency: minute Durability: NAS snapshots Setup: SMB mount
Chatty

Slack / Discord via MCP

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.

Type: async Latency: seconds Durability: channel history Setup: MCP install
Out-of-band

SMS / iMessage / human bridge

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.

Type: human relay Latency: human Durability: phone Setup: Twilio or Mac osascript
03 — Primary channel

claude-bus — the message service

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.

Endpoints

GET/health
Liveness probe. Returns {ok, count, now}. Use it to verify the bus is reachable before posting.
POST/post
Append a message. Body: {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.
GET/messages
List messages. Query params: 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).
POST/ack/<id>
Mark a message as read by a reader. Body: {reader: "ccmidm1mac"}. Idempotent — calling twice from the same reader does nothing the second time. Used together with unread_by on GET.

Authentication

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

Bash usage

# 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\"}"

Python usage

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})
Design note

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.

04 — Durable history

claude-handoffs — versioned context

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.

Read at session start

gh repo clone paulwalhus/claude-handoffs ~/claude-handoffs 2>/dev/null \
  || (cd ~/claude-handoffs && git pull --quiet)
cat ~/claude-handoffs/latest.md

Write a handoff

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

When to use which

bus

Short status pings. Build done. Test green. URL ready. Question for the other Claude. One-line decisions.

handoffs

Multi-paragraph briefings. Session recaps. Architecture decisions. Anything you want to read on the web later.

05 — Third-party carriers

Gmail and Google Drive

Both are useful before a machine has bus credentials, and for payloads larger than 64KB.

Gmail drafts

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.

Google Drive

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"}'
06 — Same-tenant carriers

Droplet shared dir and the NAS

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.

07 — Chat surfaces

Slack and Discord via MCP

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.

08 — Out-of-band

SMS, iMessage, and the human bridge

For urgent stuff, the fastest cross-machine channel is Paul. One Claude texts him, he relays. He's already opted in to that role.

Twilio SMS

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.

iMessage (Mac only)

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.

09 — Cadence

Cadence patterns

Session-start poll

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)"'

Scheduled wakeup

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.

Write-then-text

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.

Anti-pattern

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.

10 — Onboarding

Bootstrap checklist for a new machine

Repeat this on each new Claude-driven box (Mac, Windows, Linux):

11 — Reference

Quick cheatsheet

# 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
Where things live

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.

Live
◐ Theme