bridge-push (hybrid)
Move files to the VPS with a spoken handshake. Coordination rides Claude's
built-in Remote Control (ListAgents + SendMessage) — no relay, no SSH tunnel
for the talking. The bytes ride a real transport that preserves them
exactly: git (preferred) or rsync/tar over SSH. Both ends verify a manifest.
Why not send files over SendMessage? That channel carries text through an LLM,
so exact bytes aren't guaranteed. Inline only tiny text files, and only with a
sha check (see "Inline escape hatch"). For a real folder, use git or rsync.
Transport-agnostic helpers in scripts/:
bridge-transfer.sh <src> <ssh_host> <dest> [--delete] → the atomic byte-path:
opens ONE ssh connection, transfers, verifies every file's checksum on the VPS,
then closes the connection. Exit 0 = VERIFIED, 1 = MISMATCH. Prefer this.
bridge-manifest.sh <path> → FILES/BYTES/SHA fingerprint (for manual checks).
bridge-push.sh <src> <ssh_host> <dest> [method] [--delete] → transfer only, no verify.
bridge-recv-prep.sh <dest> → VPS: create dest, report free space + readiness.
Preconditions (check once)
- Find the peer:
ListAgents. Identify the VPS session (ask the user which
one if the name isn't obvious, e.g. vps-myapp). Note its name/id.
- Is it online? Remote Control sessions show
offline when not connected.
If offline → tell the user to open/connect the VPS Claude session (Remote
Control on). Do NOT proceed blind. (If it must work offline, fall back to the
bridge relay — see "Offline fallback".)
- Byte transport available? Prefer git if the app is a repo both can reach.
Else confirm
ssh <alias> true works from local for rsync.
Shared journal (MCP)
If the journal MCP server is registered, use it as the source of truth:
journal_read() at the start, claim(who, "push:<dest>") before transferring
(so two pushes don't collide), journal_log(who, "PUSH", "<name>@<sha>", "VERIFIED")
after a verified transfer, then release. who is your identity (local/vps).
Coordination is turn-based
SendMessage hands control to the peer; you resume when it replies. So the
handshake is: you send → your turn ends → the VPS Claude acts and SendMessages
back → you're re-invoked with the reply. Always tell the peer who to reply to
(your session name from ListAgents) inside the message.
Workflow — SENDER (local, has the files)
- Fingerprint:
scripts/bridge-manifest.sh <local_path> → keep FILES/BYTES/SHA.
- Request via
SendMessage({to: "<vps-session>", message: ...}):
"PUSH_REQUEST name= files= bytes= sha=. Where on the VPS
should this go, are you ready to receive, and is it a git repo (so I can push
a branch)? Reply to session <my-name>."
- (Turn ends.) On the VPS's reply —
READY dest=… [git remote=…] ssh_hint=…:
- Move the bytes + verify in one shot (SSH path, preferred when you have SSH):
scripts/bridge-transfer.sh <local_path> <ssh_host> <dest> — this opens the
SSH connection, transfers, verifies every file arrived (remote checksums ==
local), and closes the connection. Read its exit code:
- exit 0 / VERIFIED → the files are on the VPS and proven complete. You did
the verification yourself over SSH; the VPS Claude need not re-check.
- exit 1 / MISMATCH → re-run it (rsync resumes); if it keeps failing, surface
the error to the user (disk full? permissions?).
(
--delete only if the user wants the remote to mirror the source exactly.)
git path instead: commit + push the agreed branch/sha; SendMessage the VPS
"pushed to /, run git pull" and let it verify its pull.
- Tell the peer you're done and continue:
SendMessage → "VERIFIED ✅ files at
/ (sha=, files), ssh closed. Continuing." Then resume your
own work — the transfer is closed out.
- If you used the git path and the VPS reports
MISMATCH, re-push and let it
re-pull. Report the final state to the user.
Workflow — RECEIVER (VPS, says hi)
You are re-invoked by an incoming SendMessage.
- On
PUSH_REQUEST: pick/confirm a destination, scripts/bridge-recv-prep.sh <dest>
(free space + readiness). Create only the empty dest — change nothing else.
SendMessage back to the sender's session:
"hi from vps 👋 push to dest= (git remote=<…> if repo). I'm ready.
Expecting files sha=."
- On
PUSHED …: pull the bytes (git pull, or it already arrived via rsync),
then verify: scripts/bridge-manifest.sh <dest>/<name> vs the sha you were
sent. SendMessage:
- match → "RECEIVED ✅ /, files, sha matches."
- mismatch → "MISMATCH ❌ got /, expected /. Re-send."
Inline escape hatch (tiny text files only)
For 1–3 small text/code files with no git/ssh handy:
- Sender: include each file's path + contents in the
SendMessage body, and the
per-file sha (shasum -a 256 file).
- Receiver:
Write each file, then shasum -a 256 it and compare. If any sha
differs, reject and ask for git/rsync instead — the text channel corrupted it.
Never do this for binaries, many files, or anything you can't eyeball-verify.
Offline fallback (peer not connected)
If the VPS session is offline and can't be brought online, use the durable
bridge relay for coordination instead of SendMessage (same handshake, via the
bridge CLI). Bytes still go over git/rsync. See claude-bridge README.
Guardrails
- Never overwrite silently. If
<dest>/<name> exists on the VPS, say so and
ask before pushing (especially with --delete).
- Manifest is truth. "PUSHED" ≠ "received"; only a sha match closes it.
- Secrets: flag
.env/keys before pushing. .git is skipped by the manifest
but IS transferred by rsync/tar — say so if it matters.
- If the peer can't be found/confirmed online, or SSH/git fails, STOP and surface
it — don't transfer blind.
1---2name: bridge-push3description: Use when the user wants to send/push/transfer/upload a folder or files from the local machine to the VPS (or pull the other way) and a peer Claude Code runs on the VPS. HYBRID model — the two instances coordinate over Claude's built-in Remote Control (SendMessage), and the file bytes move over git (preferred) or rsync/tar-over-SSH, verified with a manifest. Triggers on "push this folder to the VPS", "send files to the VPS", "transfer <dir> to the server", "upload to VPS", "copy to the remote".4---56# bridge-push (hybrid)78Move files to the VPS with a spoken handshake. **Coordination rides Claude's9built-in Remote Control (`ListAgents` + `SendMessage`) — no relay, no SSH tunnel10for the talking.** The **bytes** ride a real transport that preserves them11exactly: git (preferred) or rsync/tar over SSH. Both ends verify a manifest.1213> Why not send files over SendMessage? That channel carries text through an LLM,14> so exact bytes aren't guaranteed. Inline only tiny text files, and only with a15> sha check (see "Inline escape hatch"). For a real folder, use git or rsync.1617Transport-agnostic helpers in `scripts/`:18- `bridge-transfer.sh <src> <ssh_host> <dest> [--delete]` → **the atomic byte-path**:19 opens ONE ssh connection, transfers, verifies every file's checksum on the VPS,20 then closes the connection. Exit 0 = VERIFIED, 1 = MISMATCH. Prefer this.21- `bridge-manifest.sh <path>` → `FILES/BYTES/SHA` fingerprint (for manual checks).22- `bridge-push.sh <src> <ssh_host> <dest> [method] [--delete]` → transfer only, no verify.23- `bridge-recv-prep.sh <dest>` → VPS: create dest, report free space + readiness.2425## Preconditions (check once)261. **Find the peer:** `ListAgents`. Identify the VPS session (ask the user which27 one if the name isn't obvious, e.g. `vps-myapp`). Note its name/id.282. **Is it online?** Remote Control sessions show `offline` when not connected.29 If offline → tell the user to open/connect the VPS Claude session (Remote30 Control on). Do NOT proceed blind. (If it must work offline, fall back to the31 bridge relay — see "Offline fallback".)323. **Byte transport available?** Prefer git if the app is a repo both can reach.33 Else confirm `ssh <alias> true` works from local for rsync.3435## Shared journal (MCP)36If the `journal` MCP server is registered, use it as the source of truth:37`journal_read()` at the start, `claim(who, "push:<dest>")` before transferring38(so two pushes don't collide), `journal_log(who, "PUSH", "<name>@<sha>", "VERIFIED")`39after a verified transfer, then `release`. `who` is your identity (local/vps).4041## Coordination is turn-based42`SendMessage` hands control to the peer; you resume when it replies. So the43handshake is: you send → your turn ends → the VPS Claude acts and SendMessages44back → you're re-invoked with the reply. Always tell the peer **who to reply to**45(your session name from `ListAgents`) inside the message.4647## Workflow — SENDER (local, has the files)481. **Fingerprint:** `scripts/bridge-manifest.sh <local_path>` → keep `FILES/BYTES/SHA`.492. **Request** via `SendMessage({to: "<vps-session>", message: ...})`:50 > "PUSH_REQUEST name=<basename> files=<n> bytes=<b> sha=<sha>. Where on the VPS51 > should this go, are you ready to receive, and is it a git repo (so I can push52 > a branch)? Reply to session `<my-name>`."533. (Turn ends.) On the VPS's reply — `READY dest=… [git remote=…] ssh_hint=…`:544. **Move the bytes + verify in one shot (SSH path, preferred when you have SSH):**55 `scripts/bridge-transfer.sh <local_path> <ssh_host> <dest>` — this opens the56 SSH connection, transfers, verifies every file arrived (remote checksums ==57 local), and closes the connection. Read its exit code:58 - **exit 0 / VERIFIED** → the files are on the VPS and proven complete. You did59 the verification yourself over SSH; the VPS Claude need not re-check.60 - **exit 1 / MISMATCH** → re-run it (rsync resumes); if it keeps failing, surface61 the error to the user (disk full? permissions?).62 (`--delete` only if the user wants the remote to mirror the source exactly.)63 **git path instead:** commit + push the agreed branch/sha; `SendMessage` the VPS64 "pushed <sha> to <remote>/<branch>, run `git pull`" and let it verify its pull.655. **Tell the peer you're done and continue:** `SendMessage` → "VERIFIED ✅ files at66 <dest>/<name> (sha=<sha>, <n> files), ssh closed. Continuing." Then resume your67 own work — the transfer is closed out.686. If you used the git path and the VPS reports `MISMATCH`, re-push and let it69 re-pull. Report the final state to the user.7071## Workflow — RECEIVER (VPS, says hi)72You are re-invoked by an incoming `SendMessage`.731. On `PUSH_REQUEST`: pick/confirm a destination, `scripts/bridge-recv-prep.sh <dest>`74 (free space + readiness). **Create only the empty dest — change nothing else.**75 `SendMessage` back to the sender's session:76 > "hi from vps 👋 push to dest=<dest> (git remote=<…> if repo). I'm ready.77 > Expecting <n> files sha=<sha>."782. On `PUSHED …`: pull the bytes (`git pull`, or it already arrived via rsync),79 then **verify:** `scripts/bridge-manifest.sh <dest>/<name>` vs the sha you were80 sent. `SendMessage`:81 - match → "RECEIVED ✅ <dest>/<name>, <n> files, sha matches."82 - mismatch → "MISMATCH ❌ got <x>/<y>, expected <n>/<sha>. Re-send."8384## Inline escape hatch (tiny text files only)85For 1–3 small text/code files with no git/ssh handy:86- Sender: include each file's path + contents in the `SendMessage` body, and the87 per-file sha (`shasum -a 256 file`).88- Receiver: `Write` each file, then `shasum -a 256` it and compare. If any sha89 differs, reject and ask for git/rsync instead — the text channel corrupted it.90Never do this for binaries, many files, or anything you can't eyeball-verify.9192## Offline fallback (peer not connected)93If the VPS session is `offline` and can't be brought online, use the durable94bridge relay for coordination instead of `SendMessage` (same handshake, via the95`bridge` CLI). Bytes still go over git/rsync. See claude-bridge README.9697## Guardrails98- **Never overwrite silently.** If `<dest>/<name>` exists on the VPS, say so and99 ask before pushing (especially with `--delete`).100- **Manifest is truth.** "PUSHED" ≠ "received"; only a sha match closes it.101- **Secrets:** flag `.env`/keys before pushing. `.git` is skipped by the manifest102 but IS transferred by rsync/tar — say so if it matters.103- If the peer can't be found/confirmed online, or SSH/git fails, STOP and surface104 it — don't transfer blind.