Skip to content

Remote Access

Roy Padina edited this page Aug 30, 2026 · 3 revisions

Remote Access

The daemon binds 0.0.0.0 and the only authentication is the token (or an invite link) plus the host's own /accept. Never put that on a public IP you advertise. Everything below is a way of moving the bytes; none of it adds a second factor.

The four routes, worst to best

route how what it costs
LAN the printed ws://<host-ip>:7777 line. Works with no extra software everyone has to be on the same network
SSH tunnel ssh -L 7777:127.0.0.1:7777 host, then claude-jam join ws://127.0.0.1:7777 … the guest needs an SSH account on the host
Tailscale install Tailscale on both machines. If it is installed, the printed join line already uses the tailnet IP both sides join the tailnet
a public relay --tunnel or --funnel, below the URL is the only thing between the internet and your session — keep it private

--tunnel — Cloudflare quick tunnel

Needs cloudflared on PATH.

claude-jam host --name Roy --cwd . --tunnel

Spawns two quick tunnels (one for the WebSocket, one for the browser view if --view is on) and prints wss://<random-words>.trycloudflare.com join and view URLs. Your guest installs nothing.

The hostname is random and changes on every respawn. That is the one real gotcha: an invite link fixes its addresses at the moment it is minted, so links minted before a respawn reach the jam only over their LAN address. Mint fresh ones, or use --funnel.

--funnel — Tailscale Funnel

Needs tailscale on PATH and Funnel enabled for the tailnet.

claude-jam host --name Roy --cwd . --funnel

Same job, but the URL is your node's own name — wss://<machine>.<tailnet>.ts.net, and https://…:8443 for the view — so it is the same every run. Your guest still installs nothing and does not need to be on your tailnet.

Two things need a human, once:

  1. Grant the funnel node attribute for the tailnet in Tailscale's Access Controls. Without it the CLI refuses and claude-jam repeats the reason verbatim.
  2. Use the standalone Tailscale build on macOS. The App Store build is sandboxed and cannot change funnel configuration at all — its CLI answers The Tailscale GUI failed to start … (Tailscale.CLIError error 3.). The build from tailscale.com can. --funnel-cli <path> points at a CLI elsewhere.

--tunnel is always available as the fallback.

Known risk, upstream. Tailscale issue #18827 — open since 2026-02-27 — reports WebSockets through tailscale serve's HTTP reverse proxy, the layer Funnel rides, closing every 10–40 s with code 1001 "Going Away". claude-jam pings every 30 s, which clears Cloudflare's 100 s idle cap but cannot outrun a 10 s drop, and no jam has yet been run over Funnel end to end. So take --funnel for the property it is proven to have — a URL that never changes — and prefer --tunnel for a long sitting until somebody proves otherwise.

Switching while the jam runs

Neither relay is launch-only. Three surfaces, one code path:

claude-jam remote off|tunnel|funnel [--jam NAME] [--reissue]   # from a shell
  • /remote off|tunnel|funnel inside the host's client
  • /menu → Access → Remote

Nobody already connected is dropped. A mode that cannot run here says why, with the exact fix. When a relay comes up, host clients get tunnel ready: <the whole join command> · give it a few seconds — the edge needs a moment before the first join works rather than a silent refresh.

Those few seconds are measured, not a hedge. cloudflared reports the hostname as soon as it has one; the Cloudflare edge needs another ~2.5 s before it will route to it. Timed on a 2 h 10 m tunnel soak, 2026-08-30:

21:10:51.616  tunnel up: sally-consideration-visitor-autos.trycloudflare.com
21:10:51.922  guest socket error, then closed code=1006      (306 ms later)
21:10:54.142  guest connected                     (2.53 s after the URL was published)

A person pasting a link into a chat is slower than that, and a client reconnects on its own, so in practice it costs nothing — it was the entire explanation for that soak's single reconnect. It matters if you are scripting a join: sleep a few seconds after the line appears, or expect one 1006 and a retry.

Links minted earlier carry the old address, so the switch offers --reissue to mint every live link again — and it waits for the new hostname first, because minting immediately would hand out exactly the address it is replacing. claude-jam invites shows the result.

Only one relay at a time: two for one port is a startup error, and the runtime switch keeps that rule.

The browser view

claude-jam host … --view

Needs ttyd. Serves the live TUI read-only at http://jam:<token>@<host>:7778 (port + 1 by default, --view-port moves it). Each tab gets its own grouped tmux session, so viewers never move each other's screen. Over --tunnel or --funnel the view gets its own URL.

What breaks over a long session

  • A quick tunnel's hostname changes when cloudflared respawns. Guests already connected keep their socket; guests reconnecting see still retrying — the join URL changed. The host runs /join and sends the new line, or switches to --funnel. There is a respawn backoff so a flapping relay does not hammer anything.
  • Sockets are heartbeated every 30 s. A missed pong shows in the status row as ⚠ stale Ns; the client reconnects on its own, in tiers, so a laptop that slept comes back without anybody typing anything.
  • A link's addresses are fixed at mint time. A link carries the relay address first and the LAN address second, and the client tries them in order with a 3-second timeout each — so a link whose tunnel is gone still works on the LAN.
  • Revoking or expiring a link disconnects nobody already in. It only stops the next join. /kick <name> revoke is the pair that actually removes somebody and takes their link back.

Next

Security Model · Hosting a Jam · Troubleshooting

Clone this wiki locally