loom Docs Remote access
Remote access
Reach your fleet from another device — without opening it up.
By default the Loom daemon binds loopback only
(127.0.0.1:4317): nothing outside your machine can reach
it. That's the right default, but sometimes you want to open the
cockpit from a laptop, a phone, or another box. There are two supported
ways to do that, and they trade off differently.
Tailscale Serve / SSH
The daemon stays loopback-only. Tailscale (or an SSH tunnel) authenticates and encrypts the transport, so Loom opens no new listening surface of its own — nothing to mint, nothing to expose.
Direct authenticated bind
Loom itself binds a non-loopback interface, gated by a gateway token and — off a tailnet — mandatory TLS. Reach for this when you can't run Tailscale or SSH on both ends.
Recommended
Tailscale Serve, or an SSH tunnel.
This is the path we recommend for almost everyone. You leave
Loom's remoteAccess config untouched (still disabled,
still loopback), and put the authentication in front of it. Because
the proxy connects to the daemon over 127.0.0.1, the
request arrives on the loopback interface Loom already trusts — you
add reach without adding an exposed port.
Tailscale Serve
If your machines are on a Tailscale
tailnet, tailscale serve publishes an HTTPS endpoint on
your node — with a certificate Tailscale provisions for you — and
proxies it to the daemon on loopback. Only devices on your tailnet
can reach it.
# proxy an HTTPS tailnet endpoint to the loopback daemon
$ tailscale serve --bg 4317
# then open the cockpit from any device on your tailnet
› https://your-machine.your-tailnet.ts.net/
# stop serving when you're done
$ tailscale serve --bg 4317 off
SSH tunnel
No tailnet? Any SSH connection to the daemon's host works just as well. Forward a local port over SSH and browse it as if it were local — SSH authenticates and encrypts the whole hop, and the daemon still only ever sees a loopback connection.
# from your laptop: forward local 4317 → the daemon host's loopback
$ ssh -N -L 4317:127.0.0.1:4317 you@daemon-host
# then open the cockpit locally
› http://127.0.0.1:4317/
Why this is the default recommendation
With Serve or SSH, the daemon's listening surface never changes — it
stays bound to 127.0.0.1. There's no gateway token to
mint, no TLS material to manage inside Loom, and no interface exposed
for anyone to probe. The transport's own identity layer (your
tailnet, or your SSH keys) does the authenticating.
Alternative
Direct authenticated bind.
When Tailscale and SSH aren't options — say you want the daemon to answer directly on a LAN interface, or on a host that's already reachable — Loom can bind a non-loopback interface itself. This opens a real listening socket, so Loom gates it hard: a gateway token is required on every remote request, and off a tailnet, valid TLS is mandatory or the daemon refuses to open the interface at all.
It's a co-equal, fully supported path — just one that puts the authentication and the certificate in your hands instead of the transport's. Prefer it over Serve/SSH only when you specifically need Loom to be the thing listening.
-
Mint a gateway token
The gateway token is the daemon-global credential that authorizes a remote request. Mint one over the loopback API. The
plaintextsecret is returned once — store it now; it's hashed at rest and never recoverable again.$ curl -X POST http://127.0.0.1:4317/api/gateway-tokens \ -H 'content-type: application/json' -d '{"name":"my-laptop"}'# → { "token": { "id": "…", "name": "my-laptop", … }, "plaintext": "<the secret — shown once>" }
-
Point Loom at your TLS cert and key
Off a tailnet, a non-loopback bind must be HTTPS. Provide a certificate and private key — from your own CA, an internal PKI, or a tool like mkcert for a LAN. You'll pass their paths as
remoteAccess.tls.certPathandkeyPathin the next step. -
Enable the remote bind
Set the daemon-global
remoteAccessconfig over the loopback API: turn it on, choose the interface to bind, and supply the TLS paths.remoteAccessis daemon-global (not per-project), so this is a one-time setting.$ curl -X PATCH http://127.0.0.1:4317/api/platform/config \ -H 'content-type: application/json' \ -d '{"config":{"remoteAccess":{ "enabled": true, "bindHost": "0.0.0.0", "tls": { "certPath": "/etc/loom/cert.pem", "keyPath": "/etc/loom/key.pem" } }}}'# bindHost can be a specific address (e.g. 192.168.1.10) or 0.0.0.0 for all interfaces
-
Restart the daemon
remoteAccessis read once at boot, so the change takes effect on the next restart. On boot Loom runs a fail-closed check: it only opens the remote interface when it's enabled, thebindHostis non-loopback, a gateway token exists, and the TLS mandate is satisfied. If any of those is missing it logs why and stays on loopback — it never opens a public interface as plain HTTP.$ loom restart
Using the token
Present the token from your client.
Once the remote interface is live, every remote request carries the
gateway token. Standard REST calls send it as a bearer token; the
live terminal WebSockets — which can't set an
Authorization header on the upgrade — accept it as a
WebSocket subprotocol or a ?token= query parameter.
# REST: Authorization: Bearer <plaintext>
$ curl https://your-host:4317/api/projects \
-H 'Authorization: Bearer <plaintext>'
# WebSocket terminals: token via the Sec-WebSocket-Protocol subprotocol, or ?token=<plaintext>
What a remote token can reach
A remote request is deliberately limited to a read-and-steer surface: the read-only views, the answer/steer actions on an already-running session (answer a question, send input, stop, resume, end), and the two read-only terminal WebSockets. Everything else — vault and git writers, the internal control endpoints, the MCP mounts, and even the gateway-token admin itself — stays loopback-only by construction. A valid remote token can never mint, rotate, or revoke tokens over the wire; only the human at the local machine can. Remote requests are also rate-limited per IP and per token, with an automatic lockout after repeated auth failures; loopback traffic is exempt.
TLS off the tailnet
TLS is mandatory for a non-tailnet bind.
A direct bind to any non-loopback, non-tailnet interface carries
traffic — including the gateway token — over an untrusted network, so
Loom requires it to be encrypted. If remoteAccess.tls is
missing, or the cert/key can't actually be read and loaded as valid
TLS material, the boot-time check fails closed: the daemon
refuses to open the remote interface and falls back to
loopback, logging exactly why. It will never bind a public
interface believing it's HTTPS when it's really plain HTTP.
A Tailscale .ts.net address is the one exception: the
tailnet already encrypts the link end to end, so a bind whose
bindHost is a .ts.net name skips the TLS
requirement. (Note this is the direct-bind-to-a-tailnet-address
case — distinct from the recommended Tailscale Serve path above, which
needs no bind at all.)
Managing tokens
Rotate, pause, revoke.
All token management is loopback-only and human-only. List shows metadata only — never the secret.
# list tokens (metadata only — no secret)
$ curl http://127.0.0.1:4317/api/gateway-tokens
# rotate a token's secret — old plaintext dies, new one returned once
$ curl -X POST http://127.0.0.1:4317/api/gateway-tokens/<id>/rotate
# pause or revoke (soft, reversible → active|paused|revoked)
$ curl -X POST http://127.0.0.1:4317/api/gateway-tokens/<id> \
-H 'content-type: application/json' -d '{"status":"revoked"}'
# delete permanently
$ curl -X DELETE http://127.0.0.1:4317/api/gateway-tokens/<id>
Back to the overview.
Remote access is optional — Loom runs perfectly as a local-only instrument. When you do open it up, keep it on the transport you trust.