Some AI coding work fits the laptop. Some doesn’t. Compiling a Rust monorepo, running a big test suite, indexing a giant codebase — you want a real dev box doing the work, with a slim client on your laptop or phone.
Ringlet’s remote-session feature is that slim client. The agent lives on the server. You drive it from your browser.
The shape of it
┌─────────────────┐ ┌──────────────────────────┐
│ your laptop │ WebSocket (TLS) │ your dev box │
│ ─────────── │ ─────────────────▶│ ──────────── │
│ ringlet client │ │ ringletd (daemon) │
│ or browser │ ◀─────────────────│ ↓ exec inside sandbox │
│ │ PTY bytes │ claude / codex / ... │
└─────────────────┘ └──────────────────────────┘
The daemon on the dev box listens for authenticated WebSocket connections, allocates a PTY, execs the agent inside a sandbox, and proxies bytes both ways. On the client side, xterm.js renders the PTY as a terminal in the browser. Or you can use ringlet profiles run … --remote from the CLI; the daemon handles both shapes the same way.
Setting it up
On the dev box:
# Install (same as the local install)
curl -fsSL https://raw.githubusercontent.com/neul-labs/ringlet/main/install.sh | sh
# Run the daemon (binds to 127.0.0.1:8765 by default)
ringlet daemon --stay-alive
# To accept connections from outside, listen on the LAN interface:
ringlet daemon --stay-alive --listen 0.0.0.0:8765
Grab the daemon’s bearer token:
cat ~/.config/ringlet/http_token
# → 7f4a8b2c-...
On your laptop:
ringlet remotes add dev-box --url http://dev-box.local:8765 --token 7f4a8b2c-...
ringlet profiles run work --remote dev-box
Or, if you’d rather a browser:
http://dev-box.local:8765
# Auth dialog appears, paste the bearer token
# Dashboard shows profiles, click "Run" on the one you want
What happens at exec
- Client connects to
wss://dev-box.local:8765/api/sessionwith the bearer token inSec-WebSocket-Protocol. - Daemon allocates a PTY pair.
- Daemon builds the sandbox config for the profile (see Sandboxing AI coding agents).
- Daemon execs the agent inside
bwrap(Linux) orsandbox-exec(macOS), with the PTY as stdin/stdout/stderr. - Daemon proxies bytes between the WebSocket and the PTY.
- Resize events (terminal window resize on your laptop) are sent as control frames; the daemon ioctl-resizes the PTY.
The agent is none the wiser. From its point of view, it’s a normal interactive terminal.
Why a daemon and not just SSH + tmux?
Three reasons we kept hearing from the people who asked for this:
- Phones and tablets. SSH from an iPad to a dev box exists but is mediocre. A browser-based terminal is just better.
- Multi-session bookkeeping. A daemon keeps track of which profiles are running, who’s connected, and what they’ve done. SSH + tmux gives you screen sessions; the daemon gives you a queryable index.
- Sandboxing. Wrapping every
tmux new-sessioninbwrapis doable but fragile. The daemon does it consistently because it’s the only thing that exec’s the agent.
You can still SSH in and use ringlet profiles run directly — the daemon doesn’t replace SSH. But the WebSocket-based path is more pleasant for the common case.
Reattach and detach
The daemon keeps the PTY alive even when no client is connected. Disconnect your laptop, the agent keeps running. Reconnect later, the buffer is replayed:
# List active sessions
ringlet remotes sessions dev-box
# SESSION_ID PROFILE AGENT UPTIME CLIENT
# 7a2c-... work claude 12m (disconnected)
# Attach to an existing session
ringlet remotes attach 7a2c-...
For long-running tasks (“index this monorepo, post the report to Slack”), this is the killer feature. Kick it off, close the laptop, attach later.
Security defaults
- TLS. In production, terminate TLS at a reverse proxy or pass
--tls-cert/--tls-keyto the daemon. Without TLS, the bearer token is sent in plaintext on first WebSocket handshake — fine forlocalhost, not fine across an untrusted network. - Bearer token only. No password auth. The token is the credential. Rotate with
ringlet daemon rotate-token. - Localhost by default. Unless you set
--listen 0.0.0.0, the daemon is unreachable from anywhere except the same host. - Sandbox always-on for remote. Remote sessions are sandboxed by default. Override per-profile if you must.
Performance
PTY bytes over a WebSocket are cheap. We’ve tested with:
- 60 fps interactive use (typing, completions): unmeasurable overhead.
- Large outputs (compiling Rust with full warnings): the WebSocket is the bottleneck only if you’re already pushing >10MB/sec — which means you’d see the same speed locally.
- Many simultaneous sessions: tested up to 20 active PTYs per daemon. The daemon is single-process Tokio; CPU stays well under 1 core.
If you’re running an AI coding agent on a beefy dev box and the WebSocket is the bottleneck, something is off.
Where it doesn’t fit
- Strict network policies. If the daemon can’t be reached from your laptop (corporate VPN, no port forwarding), you’re back to SSH. Eventually we’ll ship an outbound-tunnel mode.
- Strong multi-tenant isolation. The sandbox per-session is good. For “untrusted user with their own root login,” use containers and run the daemon inside.
- Mobile-first heavy use. xterm.js on a phone browser is workable, not delightful. We use the Tauri-packaged desktop app on laptops.
What’s next
The remote feature is the youngest part of Ringlet — shipping at 0.1.0 but newer than profiles and cost tracking. Near-term improvements:
- Outbound-tunnel mode (no inbound port required on the dev box).
- Persistent recordings (replay a session like asciinema).
- Multi-client view (two people watching the same agent run).
If any of those would change how you’d use it, file an issue on GitHub. The priorities follow the noise.