The lid shuts with no monitor and no dongle attached — and the windows keep streaming, live and controllable, into a browser.
Real capture, unedited. The phone picks a window, types into it — the words appear in the real app on the Mac — then mirrors the whole desktop. Native capture, streamed peer-to-peer.
H.264 down the session's own DataChannel — peer-to-peer, about 2.6 Mbps. Relay-only viewers get the same video at 30 fps.
The fastest SSH you'll ever configure — because there's nothing to configure.
` on another pulls the file down — Mac to Linux, Windows to Mac, laptop to server, anywhere to anywhere. End-to-end encrypted, no cloud drive, no account. AirDrop, for every machine you own.
---
## The last thing you'll install standing at your computer
Set it up once, in person — then you never have to sit at that machine again. From any browser you get its terminal, any window, the whole desktop, a public link to a local port, files to and from it, even a live session shared with someone else. One tool, every remote job.
---
## How it works
Your machine dials **out** to a relay over WSS; viewers dial out to the same relay. Nothing ever listens on your machine. Everything through the relay is encrypted end-to-end — it routes ciphertext it cannot read. Window and desktop frames don't even take that path: they ride a direct WebRTC connection between browser and host.
> You trust Cloudflare to deliver packets — the same way you trust your ISP with SSH traffic. Neither can read what you send. The difference: **reminal never opens your machine to the internet.**
---
## Everything you get
Join a session from anywhere — phone (scan the QR), any browser (open the URL, type the PIN), or another terminal (`reminal --connect --pin `). Then:
#### Persistent, resilient shell
Close the laptop, switch to your phone, reconnect from a different city — your shell is right where you left it, and the current screen paints **instantly** (a snapshot, no slow fast-forward). Wi-Fi drop, tunnel, elevator? Auto-reconnect with backoff, 2 MiB of scrollback intact.
#### Pair with anyone
Send a session ID and PIN to a teammate over any channel and they join the same live shell — or a window mirror — from a browser. No account, no install, multiple viewers at once. Ctrl+C ends it; there's nothing to revoke.
#### Sessions that outlive your terminal
Kick off a long job and close the lid — it keeps running. `reminal new deploy` spawns a named session; `list` · `attach` · `rename` · `kill` · `prune` manage the whole fleet by name, id, or fuzzy match. The Host panel shows live CPU/memory and spawns one in a tap.
#### Zero-install web terminal
A full xterm.js terminal is built into the relay. Any browser is the client — phone, iPad, locked-down work laptop, hotel-lobby PC. Pinch-zoom, text selection with draggable handles, on-screen modifier keys, voice dictation, find-in-scrollback.
#### Files, ports & pings
`reminal copy` / `paste` move a file between any two machines with a one-time code. `reminal expose 3000` puts a local port on a public, PIN-gated URL. `reminal send` pushes a file to **every viewer at once**, and `reminal notify` fires a browser notification on all of them.
#### Secure by construction
No open ports, ephemeral session ID + PIN, AES-256-GCM end-to-end with a PIN-authenticated handshake the relay can't crack offline. Ctrl+C and the credentials are gone. [Details below](#security).
#### Own a machine, skip the PIN
Enroll a device as an **owner** — `reminal own`, then `sudo reminal add owner ` once — and it connects to any of that machine's sessions with no PIN. Per-device trust, revocable one at a time (`reminal owners revoke`, or self-revoke from the browser). The relay stays blind either way.
#### Every machine, one list
`reminal machines` shows every box you own and each live session on it — what's running, viewers, idle time. Same view in the web **Machines panel**: attach, rename, spawn, or kill a session on any machine from the browser.
---
## Security
> Built to be **as secure as a properly configured SSH — and safer by default.**
SSH leaves port 22 open and trusts you to configure everything correctly. reminal takes the opposite approach: **nothing to expose, session secrets encrypted on disk, encryption end-to-end.**
| Layer | What it does |
|---|---|
| **No open ports** | Your machine only initiates outbound connections. There is nothing on the network to scan, brute-force, or zero-day. |
| **Ephemeral credentials** | Session ID and PIN exist only while `reminal` is running. Ctrl+C and they are gone forever. |
| **Owner devices, revocable** | A device you enroll as an owner connects without the PIN using its own key — a separate trust path from the ephemeral PIN, gated behind `sudo` to enroll and revocable per-device (or self-revoked from any browser). The relay still only routes ciphertext. |
| **Dual-factor by design** | An attacker needs both the session ID (~1 trillion combinations) and the 8-digit PIN. Knowing one is useless. |
| **Rate-limited by the agent** | Every PIN guess costs a full online handshake with your machine. The agent answers a few in a row, then one every few minutes, and someone who keeps guessing waits longer each time, up to an hour, so they get about 24 tries a day. At that rate, trying every 8-digit PIN takes over ten thousand years. Devices that already proved the PIN, and owner devices, never wait. |
| **End-to-end encryption** | AES-256-GCM with a fresh random 256-bit session key per agent run. Distributed to each viewer via a PIN-authenticated handshake (CPace, a standard PAKE) — the relay never sees the key or anything offline-brute-forceable from it. |
| **Forward-secret handshake** | Each WebSocket connection runs its own exchange with fresh keys. Even if a future attacker recovers the PIN, recorded ciphertext stays unreadable. |
| **Relay-blind** | Cloudflare Workers route ciphertext. A relay that records traffic cannot recover the session key offline — wrong PIN guesses are detectable only by attempting a full handshake online (one shot each, bounded by the agent's kex throttle). |
| **P2P you can trust** | WebRTC signaling (SDP, ICE) rides inside the already-encrypted session channel, so the relay can't tamper with DTLS fingerprints — no man-in-the-middle window. Frames on the DataChannel are DTLS-protected end-to-end. |
| **TLS in transit** | WSS / TLS on every hop in production. |
**One deliberate exception:** `reminal expose` port-forwards are **not** end-to-end encrypted — the visitor is an ordinary browser with no reminal key, so that traffic passes through the relay in plaintext (PIN-gated, but readable by the relay). Everything else above is E2E. Self-host the relay if that matters to you.
**Best practices:** share the session ID and PIN over different channels (email the ID, text the PIN) · Ctrl+C when done — credentials die instantly · keep the client current with `reminal upgrade`.
**Digging deeper:** [Security architecture](docs/security/architecture.md) · [Threat model](docs/security/threat-model.md) · [Subprocessors & data handling](docs/security/subprocessors.md) · [Self-assessment](docs/security/self-assessment.md) · [Report a vulnerability](SECURITY.md)
---
## reminal vs SSH, at a glance
SSH was designed in 1995 — it assumes a static IP, a router you can configure, and keys you keep rotated. reminal assumes none of that, so the trade-offs line up differently:
| | **reminal** | SSH |
|---|---|---|
| **Setup time** | One command | Keys, configs, port-forwarding, firewalls |
| **Listening port** | None | TCP 22 exposed to the internet |
| **Credentials** | Per-session ID + PIN, encrypted on disk | Permanent keys on disk |
| **Behind NAT / hotel Wi-Fi** | Just works | VPN or jump host required |
| **Client required on viewer** | None — a browser is the client | `ssh` + a configured key per device |
| **Phone friendly** | Scan QR → in | No native client |
| **Session survives disconnect** | Shell keeps running, hop between devices | Drop the connection, lose your work (unless you wrapped it in `tmux`) |
| **Network blips** | Auto-reconnect, scrollback replay | `Write failed: Broken pipe` |
| **GUI apps** | Mirror & control any window — or the whole desktop | X11 forwarding, if you dare |
| **Laptop lid shut, no monitor** | Closed-lid mode keeps serving on a virtual display | Terminal only |
| **If laptop is stolen** | Sessions already dead | Old keys still grant access |
| **Encryption** | End-to-end through relay | End-to-end direct (if configured right) |
---
## Run your own relay (free, one time)
The relay runs on **Cloudflare Workers + Durable Objects**. The free tier handles thousands of sessions a month — and window frames go peer-to-peer, so the heavy traffic never touches it.
```bash
cd cloudflare
npm install
npx wrangler login
npm run deploy
```
Then copy `reminal.build.env.example` to the gitignored
`reminal.build.env`, put your `workers.dev` URL there, and run
`./scripts/build.sh`. No source edit is needed. Full guide in
[cloudflare/README.md](cloudflare/README.md).
---
## Local development
```bash
# Build once; source builds retain the upstream public relay by default
./scripts/build.sh
# Terminal 1 — your own relay on localhost:8080
./dist/reminal relay
# Terminal 2 — share a session via the local relay
REMINAL_LOCAL=1 ./dist/reminal
# Terminal 3 — connect from another shell or the browser
REMINAL_LOCAL=1 ./dist/reminal connect
# or http://localhost:8080/?s=
```
A relay run behind a reverse proxy sees every viewer at the proxy's address.
If the proxy sets `X-Real-IP` (preferred) or appends to `X-Forwarded-For`,
start the relay with `REMINAL_RELAY_TRUST_PROXY=1` so it uses those instead:
`X-Real-IP`, or else the last `X-Forwarded-For` entry. Use it only if the proxy
overwrites `X-Real-IP` (or never passes one through from the client), and
leave it unset when the relay is reached directly, because anyone could then
set those headers.
To test against a remote relay without rebuilding, set either runtime URL;
reminal derives its counterpart automatically:
```bash
REMINAL_RELAY=wss://your-relay.example/ws ./dist/reminal
# or: REMINAL_WEB=https://your-relay.example ./dist/reminal
```
---
## Reference
### Platform support
The mirroring you see above isn't macOS-only — window capture **and** full control (click, type, scroll, drag) work on Linux/X11 and Windows as well.
| Capability | macOS | Linux | Windows |
|---|---|---|---|
| Terminal sharing · sessions · files · port forwarding | ✅ | ✅ | ✅ ConPTY |
| Owner connect (PIN-free) · `reminal machines` | ✅ | ✅ | ✅ |
| Window & desktop mirroring + control | ✅ ScreenCaptureKit — H.264 up to 60 fps | ✅ X11 — `wmctrl` · `xdotool` · ImageMagick | ✅ Win32 — PrintWindow · SendInput |
| Closed-lid mode (auto virtual display) | ✅ | — | — |
| Hot restart (`reminal restart`) | ✅ | ✅ | ✅ (foreground sessions convert to background + attached viewer) |
Linux capture needs an **X11** session (or Xwayland) — native Wayland blocks synthetic input, so it isn't supported yet. Apple Silicon, x86_64, and Windows ARM64 all supported.
#### Windows notes
- **Shell**: sessions open PowerShell 7 (`pwsh`) when installed, else Windows PowerShell, else `cmd` — set `$env:SHELL` to override. Terminals run through **ConPTY**, the same API Windows Terminal uses, so colors, TUIs, and resizing behave like a native console.
- **No permission prompts**: unlike macOS's Screen Recording grant, window mirroring and input injection need nothing enabled — it works out of the box.
- **Firewall prompt on first mirror**: when a viewer first attaches to a window/desktop pane, Windows Firewall asks about reminal — that's the direct peer-to-peer (WebRTC) stream binding a UDP port, the same prompt any video-call app gets. **Allow** enables P2P; **Cancel** is also fine — streaming falls back to the encrypted relay path.
- **Streaming**: Windows uses the JPEG capture path (~5–15 fps). The 60 fps H.264 pipeline is currently macOS-only.
- **Mirroring needs a logged-in desktop** — a machine sitting at the login screen (or a service session) has no windows to capture; terminal sharing works regardless.
- **Upgrades & hot restart**: `reminal upgrade` swaps the exe in place (the running one is renamed aside), and `reminal restart` hot-swaps a session's agent onto the new binary without touching the shell inside — each session's shell lives in a tiny ConPTY-holder process, so the agent can be replaced under it (the session's PID changes, unlike Unix). A *foreground* session restarts by converting: it moves to the background and your terminal becomes an attached viewer of it — same shell, same keystrokes, Ctrl-] detaches. The background host also restarts itself automatically after an upgrade.
### Commands
| Command | What it does |
|---|---|
| `reminal [--name ]` | Share this terminal session |
| `reminal new [name]` | Spawn a fresh background session (detached — survives this terminal closing) |
| `reminal list [filter] [-v]` | List sessions, recent-first; filter by id/name/cwd/title (`--idle`, `--viewers`, `--headless`) |
| `reminal attach [id\|name]` | Re-connect to a local session as a viewer (no arg → interactive picker) |
| `reminal connect [pin]` | Connect to a remote session from your terminal (PIN prompted if omitted) |
| `reminal rename [id\|name] ` | Rename a running session (inside a session: `reminal rename `) |
| `reminal stop [id\|name\|port]` | Stop the reminal layer — kicks viewers, keeps your shell/server running |
| `reminal kill [id\|name]` | Fully terminate a session (kills the shell — irreversible) |
| `reminal prune [dur] [-y]` | Kill idle, unwatched sessions in one go (default idle ≥ 30m) |
| `reminal restart [--all]` | Hot-swap the running agent(s) onto the latest binary — the shell stays alive |
| `reminal repin [id\|name] [--all]` | Give session(s) a new PIN — the old PIN and links stop working, the shell stays alive, owner devices are unaffected |
| `reminal integrate [--remove]` | Register reminal's MCP server with your agent CLIs (Claude Code, Codex, Cursor, Gemini, Qwen, OpenCode, Antigravity, Amp, pi) |
| `reminal mcp` | Run the MCP server on stdio — list, search, read and type into sessions across your machines |
| `reminal expose [--public] [--name ]` | Forward a local HTTP port to a public URL (PIN-protected by default) |
| `reminal send ` | Push a file to every connected viewer (web client auto-downloads) |
| `reminal copy [--ttl ] ` | Offer a file or folder for pickup anywhere; prints a one-time code |
| `reminal paste [dest]` | Fetch a file or folder offered by `reminal copy` on another machine |
| `reminal notify ` | Push a notification to viewers (browser notification on web) |
| `reminal connections` | List currently attached viewers with connect time |
| `reminal own` | Print this device's owner id + the `add owner` line to paste on machines you want to own |
| `reminal add owner [--label ]` | Enroll an owner device on this machine (needs `sudo` / an Administrator terminal on Windows) — lets it connect PIN-free |
| `reminal owners [rename\|revoke\|restore …]` | List / relabel / revoke / restore this machine's owner devices |
| `reminal machines [rename ]` | List every machine you own and its live sessions (web Machines panel manages them) |
| `reminal info [id\|name] [--all] [--qr] [--json]` | Show connect details — ID / PIN / URL / QR |
| `reminal qr [id\|name]` | Print just the join QR (for a second screen) |
| `reminal settings` | Settings page: keep the Mac unlocked for remote control; **closed-lid mode** (serve with the lid shut and nothing plugged in — disables clamshell sleep, auto-creates a virtual display while headless, darkens the built-in screen while the lid is shut) |
| `reminal doctor` | Self-diagnostic: version, relay reachability, terminal, shell |
| `reminal permissions` | macOS: grant Screen Recording to reminal once, so background (`+`) sessions can mirror windows |
| `reminal completion ` | Print a shell completion script |
| `reminal upgrade` | Upgrade to the latest release |
| `reminal relay [port]` | Start a local relay (development only) |
| `reminal version [--verbose]` | Print version |
Sessions resolve by **exact id, exact name, unique id prefix, or unique substring** of name / cwd / title — `reminal attach deploy` just works.
### Environment variables
| Variable | Default | What it does |
|---|---|---|
| `REMINAL_RELAY` | Upstream public relay | Relay WebSocket base URL; also derives `REMINAL_WEB` when that is unset |
| `REMINAL_WEB` | Upstream public web UI | Web UI URL; also derives `REMINAL_RELAY` when that is unset |
| `REMINAL_LOCAL` | — | Set to `1` to point everything at `localhost` |
| `REMINAL_OWNERS_DIR` | `/etc/reminal` (`%ProgramData%\reminal` on Windows) | Where the machine's owner list lives (the admin-gated trust store) — override for tests or unusual layouts |
| `REMINAL_NO_KEEP_AWAKE` | — | Set to `1` to let the host sleep while reminal runs (defaults to keeping it awake via `caffeinate` / `systemd-inhibit` / `SetThreadExecutionState`) |
| `REMINAL_SHELL_CGROUP` | — | Linux only: the absolute path of a cgroup v2 directory to start each session's shell in, so everything it runs is under that cgroup's limits (reminal itself stays where it is). If the shell can't be started there, it starts the usual way and the session's log says why |
| `REMINAL_TURN` / `REMINAL_TURN_USER` / `REMINAL_TURN_PASS` | — | Optional TURN server for P2P window mirroring behind hostile NATs (or `REMINAL_TURN_CF_KEY` + `REMINAL_TURN_CF_TOKEN` for Cloudflare TURN). Without one, un-punchable viewers stay on the relay fallback |
| `REMINAL_NO_CAPTURE_HELPER` | — | Set to `1` to force the screenshot capture path (skip the native ScreenCaptureKit helper) |
| `REMINAL_DEBUG` | — | Set to `1` to append the raw error string to status lines, for diagnosing connection problems |
| `SHELL` | `$SHELL`, then probes `/bin/zsh`, `/bin/bash`, `/bin/sh` (Windows: `pwsh` → `powershell` → `cmd`) | Which shell to spawn inside the session |
Installs to `~/.local/bin/reminal` (macOS/Linux) or `%LOCALAPPDATA%\Programs\reminal` (Windows) — no sudo/admin needed. Apple Silicon, x86_64, and Windows ARM64. Build from source with `./scripts/build.sh` (Go 1.25+, Swift toolchain on macOS for the native capture helper); on Windows it's a plain `go build ./cmd/reminal`.
For a persistent custom default in local builds, copy
`reminal.build.env.example` to `reminal.build.env` and set
`REMINAL_DEFAULT_RELAY` and/or `REMINAL_DEFAULT_WEB`. The local file is ignored
by git and is parsed as inert `KEY=VALUE` data (not executed as shell code).
Release workflows use repository variables with the same names, so
forks can publish their own defaults; when those variables are absent, the
upstream defaults remain intact so ordinary contributor and upstream builds
continue to work.
---
## VS Code and Cursor
There's also a reminal extension for VS Code, Cursor, Windsurf and VSCodium:
pick **reminal** from the terminal panel's `+` menu and that terminal is shared.
Get it from the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=reminal.reminal)
or [Open VSX](https://open-vsx.org/extension/reminal/reminal).
---
## Ready to try it?
```bash
curl -fsSL https://raw.githubusercontent.com/harshalgajjar/Reminal/main/install.sh | sh
reminal
```
On Windows (PowerShell):
```powershell
irm https://raw.githubusercontent.com/harshalgajjar/Reminal/main/install.ps1 | iex
reminal
```
Scan the QR and you're in. No signup, no port-forwarding, session secrets encrypted on disk. About **30 seconds** from this page to your own machine, live in a browser.
---
### License
reminal is **dual-licensed** under [AGPL-3.0](LICENSE). Using it — personally or
inside a company, unmodified — needs nothing from us. A
[commercial license](LICENSING.md) covers embedding reminal in a product you
distribute, or running a modified copy as a service. See
[`LICENSING.md`](LICENSING.md) for where that line sits, and [`CLA.md`](CLA.md)
if you'd like to contribute.
Built by @harshalgajjar. Stars are appreciated. Issues even more so.