View and control terminals from your browser with end-to-end encryption 🔒
Find a file
Chad Smith 0080d4ce47
switch tokio-tungstenite from native-tls to rustls (#112)
* rewrite backend in rust with new vanilla js frontend

- axum/tokio server with embedded static assets (rust-embed)
- aes-128-gcm e2e encryption compatible with web crypto api
- pty fork via libc::forkpty with async i/o
- short terminal ids (8 chars) and clean /s/{id}#key urls
- base64url encoding for keys (no padding)
- xterm.js bundled in binary (no cdn dependency)
- proper browser disconnect detection via watch channel
- fixes resize bug from python version (_replace no-op)
- lighthouse: wcag contrast, aria labels, focus styles, meta tags

* fix terminal dimensions display, add chadsmith.dev footer link, show initial xterm size

* cleaner status bar with dot indicator, better labels, cols x rows format, chadsmith.dev footer

* remove platform-specific copy/paste hint from terminal welcome message

* add session info prelude on browser connect: command, access mode, elapsed time

* reset terminal focus reporting mode on exit to prevent macos terminal warning

* center terminal with visible border, rounded corners, subtle shadow

* downgrade to xterm 4.19 to fix ctrl+c sending csi u sequences shells dont understand

* add dark/light theme toggle, prefill terminal id from /s/ url, fix theme css variables

* disable so_reuseaddr so binding to an already-used port fails immediately

* resolve hostname before binding, clear error message when port is in use

* print listening message after successful bind, suggest --port on conflict

* add descriptive help text for all cli flags

* add github releases workflow, curl install script, remove python ci/publish tooling

* add rust target dir to gitignore

* add ci workflow, encryption tests, cargo fmt, update docs for rust rewrite

* add --version flag to serve and share subcommands

* replace unwrap/expect panics with graceful error handling, fix clippy warnings

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* add worktree rule and autonomous pr workflow to claude.md

* rewrite in rust with single-binary distribution (#108)

* rename master references to main across docs

* cargo fmt

* remove stale static hosting and gh-pages references from readme

* rewrite readme for rust single-binary release

---------

Co-authored-by: cs01 <cs01@users.noreply.github.com>

* replace architecture diagram with ascii art (#109)

Co-authored-by: cs01 <cs01@users.noreply.github.com>

* add windows support via portable-pty, update ci and release for windows (#110)

Co-authored-by: cs01 <cs01@users.noreply.github.com>

* switch tokio-tungstenite from native-tls to rustls-tls-native-roots

---------

Co-authored-by: cs01 <cs01@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 19:04:01 -07:00
.github switch tokio-tungstenite from native-tls to rustls (#112) 2026-03-21 19:04:01 -07:00
.vscode add pre commit file and run pre-commit run --all-files (#106) 2022-06-21 23:01:40 -07:00
docs add pre commit file and run pre-commit run --all-files (#106) 2022-06-21 23:01:40 -07:00
termpair add pre commit file and run pre-commit run --all-files (#106) 2022-06-21 23:01:40 -07:00
termpair-rs switch tokio-tungstenite from native-tls to rustls (#112) 2026-03-21 19:04:01 -07:00
tests add pre commit file and run pre-commit run --all-files (#106) 2022-06-21 23:01:40 -07:00
.flake8 add pre commit file and run pre-commit run --all-files (#106) 2022-06-21 23:01:40 -07:00
.gitignore switch tokio-tungstenite from native-tls to rustls (#112) 2026-03-21 19:04:01 -07:00
.pre-commit-config.yaml add pre commit file and run pre-commit run --all-files (#106) 2022-06-21 23:01:40 -07:00
CHANGELOG.md add pre commit file and run pre-commit run --all-files (#106) 2022-06-21 23:01:40 -07:00
CLAUDE.md switch tokio-tungstenite from native-tls to rustls (#112) 2026-03-21 19:04:01 -07:00
CONTRIBUTING.md switch tokio-tungstenite from native-tls to rustls (#112) 2026-03-21 19:04:01 -07:00
Dockerfile add pre commit file and run pre-commit run --all-files (#106) 2022-06-21 23:01:40 -07:00
install.sh switch tokio-tungstenite from native-tls to rustls (#112) 2026-03-21 19:04:01 -07:00
LICENSE add pre commit file and run pre-commit run --all-files (#106) 2022-06-21 23:01:40 -07:00
makefile fix --version parsing (#87) 2021-08-30 23:22:15 -07:00
MANIFEST.in add pre commit file and run pre-commit run --all-files (#106) 2022-06-21 23:01:40 -07:00
mkdocs.yml add end-to-end encryption 2020-02-09 14:59:54 -08:00
noxfile.py switch tokio-tungstenite from native-tls to rustls (#112) 2026-03-21 19:04:01 -07:00
README.md switch tokio-tungstenite from native-tls to rustls (#112) 2026-03-21 19:04:01 -07:00
requirements.in update python and js dependencies (#43) 2021-06-01 23:26:12 -07:00
requirements.txt fix python 3.10 bug by updating dependencies(#104) 2022-06-21 22:32:31 -07:00
setup.py add pre commit file and run pre-commit run --all-files (#106) 2022-06-21 23:01:40 -07:00
termpair_browser.gif use fast api 2020-02-04 21:15:35 -08:00


View and control remote terminals from your browser with end-to-end encryption

CI

Originally written in Python, rewritten in Rust for single-binary distribution.

Features

  • End-to-end encrypted with AES-128-GCM -- the server never sees plaintext
  • Share terminals in real time (Linux, macOS, Windows)
  • Type from the terminal or browser; both are kept in sync
  • Multiple browsers can connect simultaneously
  • Read-only or read-write browser permissions
  • Single static binary with frontend embedded, no runtime dependencies
  • Terminal dimensions synced to the browser in real time

Installation

Quick Install

curl -fsSL https://raw.githubusercontent.com/cs01/termpair/main/install.sh | sh

Installs the latest binary to /usr/local/bin. Customize with environment variables:

INSTALL_DIR=~/.local/bin sh     # custom install directory
VERSION=v0.5.0 sh               # specific version

GitHub Releases

Download a prebuilt binary from the releases page. Available for Linux (x86_64, aarch64), macOS (x86_64, Apple Silicon), and Windows (x86_64).

Build from Source

git clone https://github.com/cs01/termpair.git
cd termpair/termpair-rs
cargo build --release
cp target/release/termpair /usr/local/bin/

Usage

Start the server:

termpair serve

Share your terminal:

termpair share

This prints a URL containing a unique terminal ID and encryption key. Share it with whoever you want to give access. Anyone with the link can access your terminal while the session is running.

By default, termpair share runs your $SHELL. The server multicasts terminal output to all connected browsers.

How it Works

┌─────────────────┐                                    ┌─────────────────┐
│                 │    encrypted terminal output        │                 │
│  Terminal       │───────────────────────────────────▶│  Browser(s)     │
│  (termpair      │         ┌───────────────┐          │  (xterm.js +    │
│   share)        │◀────────│  Server       │─────────▶│   Web Crypto)   │
│                 │         │  (blind relay) │          │                 │
│  - forks pty    │    encrypted browser input          │  - decrypts     │
│  - encrypts I/O │         │  never sees   │          │    output       │
│  - manages keys │         │  plaintext    │          │  - encrypts     │
│                 │         └───────────────┘          │    input        │
└─────────────────┘                                    └─────────────────┘

The server is a blind relay -- it routes encrypted WebSocket messages without access to keys or plaintext. The terminal client forks a pty, encrypts all output with AES-128-GCM, and decrypts browser input. Browsers decrypt and render with xterm.js + Web Crypto API.

Encryption

Three AES-128-GCM keys are created per session:

  1. Output key -- encrypts terminal output before sending to the server
  2. Input key -- encrypts browser input before sending to the server
  3. Bootstrap key -- delivered via the URL hash fragment (never sent to the server), used to securely exchange keys #1 and #2

Keys are rotated after 2^20 messages. IVs are monotonic counters to prevent reuse.

The browser must be in a secure context (HTTPS or localhost).

Deployment

NGINX

upstream termpair_app {
  server 127.0.0.1:8000;
}

server {
    server_name myserver.com;
    listen 443 ssl;
    ssl_certificate fullchain.pem;
    ssl_certificate_key privkey.pem;

    location /termpair/ {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;
        proxy_pass http://termpair_app/;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

systemd

# /etc/systemd/system/termpair.service
[Unit]
Description=TermPair terminal sharing server
After=network.target

[Service]
ExecStart=/usr/local/bin/termpair serve --port 8000
Restart=on-failure
RestartSec=1s

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable termpair.service
sudo systemctl restart termpair

TLS

Generate a self-signed certificate:

openssl req -newkey rsa:2048 -nodes -keyout host.key -x509 -days 365 -out host.crt -batch

Then pass it to the server:

termpair serve --certfile host.crt --keyfile host.key

CLI Reference

$ termpair serve [OPTIONS]
  -p, --port <PORT>          port to listen on [default: 8000]
      --host <HOST>          host to bind to [default: localhost]
  -c, --certfile <CERTFILE>  path to SSL certificate for HTTPS
  -k, --keyfile <KEYFILE>    path to SSL private key for HTTPS

$ termpair share [OPTIONS]
      --cmd <CMD>     command to run [default: $SHELL]
  -p, --port <PORT>   server port [default: 8000]
      --host <HOST>   server URL [default: http://localhost]
  -r, --read-only     prevent browsers from typing
  -b, --open-browser  open the share link in a browser