Reach it from a phone
Two ways to open VCode on a phone: over a Tailscale tailnet, or over a Cloudflare tunnel with Cloudflare Access in front. The server itself always binds loopback under the unit; neither route changes that.
Why you would use it
The whole point of the app is driving agents from a phone. Loopback on the machine gets you nowhere from outside it, and neither option asks you to open a port on your router.
How to use it
Over Tailscale
- Enable HTTPS certificates for the tailnet at
https://login.tailscale.com/admin/dns, "HTTPS Certificates" → Enable. It is
a tailnet-wide setting;
tailscale status --jsonshowsCertDomains: nulluntil it is on. sudo tailscale set --operator=$USERonce, soserveandcertstop needing sudo.tailscale serve --bg --https=443 http://127.0.0.1:3444tailscale serve statusprints the URL,https://<host>.<tailnet>.ts.net/.- Keep
BIND_HOST=127.0.0.1so the plain HTTP port stays loopback and every remote hit goes through the HTTPS proxy. - Open the
ts.netURL on the phone. Reverse it withtailscale serve --https=443 off.
Over a Cloudflare tunnel with Access
Do Access before DNS, so the hostname is never reachable unauthenticated, even for a few seconds.
- Zero Trust → Access → Applications → Add an application → Self-hosted. Name it, set the session duration, set the application domain to your hostname, and attach your existing allow policy rather than duplicating it.
- Zero Trust → Networks → Tunnels → your tunnel → Public Hostnames → Add a
public hostname. Subdomain and domain, empty path, service type
HTTP, URLlocalhost:3445. This one action inserts the ingress rule and creates the proxied DNS CNAME. - Put the tunnel's token into
TUNNEL_TOKENin~/.config/v-code/env. - Run
bash install.sh update. The tunnel unit is enabled and started now that the token is there. - Open the hostname on the phone. Access asks for your identity first, then the app asks for the token.
What you see
Over the tunnel you meet two gates in order. Cloudflare Access redirects you to your identity provider. Only after that does the app's own token sheet appear — see log-in-with-the-token.
When an Access session runs out while the app is open, public/store.js sees
HTML from a route that only ever speaks JSON and marks the session stale. The
app is meant to turn the offline banner into
Access session expired — tap to sign in again, whose tap reloads the page so
Access can run its redirect flow — EventSource and fetch cannot follow that
dance themselves. Today that banner never becomes visible. See the gap
below; what you actually get is an app that stops updating, and a reload by hand
puts you back through the Access login.
Options and settings
| Option | Default | What it changes |
|---|---|---|
TUNNEL_TOKEN |
(empty) | cloudflared's token. Empty leaves v-code-tunnel.service untouched |
BIND_HOST |
127.0.0.1 |
Set to a tailnet IP only when the tunnel connector runs on another machine |
VCODE_URL |
(empty) | This machine's public URL, shown in the UI and used by the node switcher |
VCODE_PEERS |
(empty) | Sibling machines as name|url, comma separated |
Limits and known gaps
- The Access expiry banner does not render.
renderExpiry()inpublic/app.jsreplaces the banner's children with a text node and addsshow access;renderOffline(), later in the same render pass, resetsbanner.className— dropping both classes — and then throwsTypeError: Cannot set properties of null (setting 'textContent')on the#offline-msgspan that is no longer there. The throw aborts the rest of every later render too, so the app freezes where it is. Reload the page by hand to get the Access login back. tailscale serveis nottailscale funnel: the result is still tailnet-only.- Plain
http://to a tailnet IP is not a secure context, so there is no service worker, no installable app and no offline mode until HTTPS is on. - Editing the tunnel's config JSON directly replaces the whole ingress array. Dropping a rule silently 404s that hostname while leaving its DNS and Access app intact. Prefer the dashboard's "Add public hostname" flow.
- cloudflared's token is on its process command line, readable by the agents
VCode spawns. That is the shape of the tunnel unit.
install.sh statusredacts it from anything it prints, but the process table still has it. - The lockout after wrong tokens trusts the client address cloudflared reports, because cloudflared is the only way in. On a machine reachable another way, a caller can fake that address.
- With
AUTH_TOKENunset, anyone who can route to the address runs commands as you. Set a token before putting this on a device you carry around.
Related
- log-in-with-the-token — the second gate
- trust-model-and-security-headers — why both gates matter
- install-as-systemd-units — where
TUNNEL_TOKENgoes - install-on-a-phone — adding it to the home screen once HTTPS is on
- open-the-app-offline — the banner this one borrows
- machine-name —
VCODE_URLandVCODE_PEERSin the UI - link-this-box — a third way: link the box to a v-code.dev account and use the app