* 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>
|
||
|---|---|---|
| .github | ||
| .vscode | ||
| docs | ||
| termpair | ||
| termpair-rs | ||
| tests | ||
| .flake8 | ||
| .gitignore | ||
| .pre-commit-config.yaml | ||
| CHANGELOG.md | ||
| CLAUDE.md | ||
| CONTRIBUTING.md | ||
| Dockerfile | ||
| install.sh | ||
| LICENSE | ||
| makefile | ||
| MANIFEST.in | ||
| mkdocs.yml | ||
| noxfile.py | ||
| README.md | ||
| requirements.in | ||
| requirements.txt | ||
| setup.py | ||
| termpair_browser.gif | ||
View and control remote terminals from your browser with end-to-end encryption
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:
- Output key -- encrypts terminal output before sending to the server
- Input key -- encrypts browser input before sending to the server
- 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
