share-terminal
The share-terminal command shares a live shell from your laptop to any browser, peer-to-peer over WebRTC. No relay server holds your terminal bytes. Once installed globally, it's available anywhere on your $PATH.
Install
Install globally with any package manager. The package is unscoped; share-terminal lands on your $PATH.
npm install -g share-terminal
pnpm add -g share-terminal
yarn global add share-terminalNative dependencies. share-terminal bundles in node-pty and node-datachannel. Both ship prebuilds for common Node + OS combos; if a prebuild isn't available, npm will compile from source — you'll need Xcode Command Line Tools on macOS or build-essential + Python on Linux.
Verify:
$ share-terminal --version
0.1.0
$ share-terminal --help
Usage: share-terminal [options] [command]
start Start a shared terminal session
login Sign in with Google (device flow)
logout Forget the stored credentialsQuick start
Sign in
$ share-terminal loginOpens your browser to Google's device verification page. The CLI polls the server until you approve, then saves your token to
~/.config/st/token.json(chmod 0600).Start a session
$ share-terminal startPrints a 6-digit pairing code in a box, with a live countdown. The CLI sits on a signaling WebSocket waiting for a viewer.
Open the web terminal
On any device, open the web app, sign in with the same Google account, and type the code. Once both sides finish the WebRTC handshake, the shell streams directly into the browser tab.
End the session
Close the browser tab, or type
exitin the shell, or hitCtrl-D. The CLI exits with the shell's exit code.
Commands
share-terminal start
Spawn $SHELL in a PTY and share it. Prints a pairing code; waits for a viewer to claim it; on success, hands the PTY over to the WebRTC DataChannel.
| Flag | What it does |
|---|---|
--echo | Don't spawn a PTY. Echo whatever the viewer sends back as a string. Used to validate the wire end-to-end without a shell. |
Lifecycle:
- Pairing code expires in 2 minutes if unclaimed — the CLI prints the countdown and exits with code
1on expiry. - When the viewer disconnects (closes the tab, network drop), the CLI kills the PTY and exits.
- When the PTY exits (you typed
exit), the CLI sends a BYE frame and exits with the shell's exit code.
share-terminal login
Runs Google's OAuth Device Authorization Grant (RFC 8628). The server brokers the request — your CLI never sees the OAuth client secret. The resulting ID token is stored at ~/.config/st/token.json (or $XDG_CONFIG_HOME/st/token.json if set).
| Flag | What it does |
|---|---|
--force | Run the flow even if a fresh token is already on disk (useful for switching accounts). |
share-terminal logout
Deletes the local token file. Does not revoke it with Google.
Configuration
Where state lives
~/.config/st/token.json | Stored Google identity (access_token, id_token, refresh_token, expires_at, sub, email). File mode 0600, dir 0700. |
$XDG_CONFIG_HOME | If set, overrides the default ~/.config base. |
Environment
| Variable | Effect |
|---|---|
SHELL | Shell to spawn inside the PTY. Falls back to /bin/sh if unset. |
XDG_CONFIG_HOME | Base dir for the token file (see above). |
The CLI does not read its own .env file. The signaling URL, Google client IDs, ICE servers, and pairing TTL are configured on the server, and returned to the CLI in the /pair response.
The wire
Terminal data and control flow as a single binary stream over one ordered+reliable WebRTC DataChannel. Frames are 1 byte of type followed by an opaque payload.
| Type | Hex | Direction | Payload |
|---|---|---|---|
| OUTPUT | 0x00 | host → viewer | Raw PTY stdout bytes (UTF-8). |
| INPUT | 0x01 | viewer → host | Raw keystroke bytes (UTF-8). |
| RESIZE | 0x02 | both | uint16 cols, uint16 rows, big-endian. Drives SIGWINCH on the PTY. |
| PING / PONG | 0x03 / 0x04 | both | uint32 nonce. Liveness probe; PONG echoes the same nonce. |
| HELLO | 0x05 | host → viewer | First frame after channel open. JSON {role, ver, term}. |
| BYE | 0x06 | both | Final frame. JSON {reason}. |
The host applies backpressure using DataChannel bufferedAmount: pauses send when above 1 MiB, resumes below 256 KiB. The viewer batches incoming OUTPUT bytes on a 16ms timer (or at 16 KB) so heavy bursts don't saturate the main thread.
Troubleshooting
“Pairing code expired”
Codes live for 2 minutes. Run share-terminal start again — old codes are dropped from Redis as soon as they expire.
“This code was created by a different Google account”
The viewer must sign in with the same Google account as the host. This is by design — see the security model below.
Connection sits at "connecting…"
Means the WebRTC handshake never completed. Usually one of:
- Restrictive NAT/firewall on one side. The server returns a STUN/TURN list in
/pair; if no TURN is configured and direct connection fails, the session can't form. - The host's clock is significantly skewed (token verification fails).
Garbled output / missing box-drawing chars
The CLI requires a UTF-8 locale (LANG=*.UTF-8). If your shell prints Latin-1 bytes, multi-byte UTF-8 won't round-trip cleanly.
“Cannot find module 'node-pty'” after install
npm couldn't download a prebuild for your Node version and failed to compile from source. Install build tooling and reinstall:
# macOS
xcode-select --install
npm install -g share-terminal
# Debian / Ubuntu
sudo apt install build-essential python3
npm install -g share-terminalSecurity model
- Terminal bytes never touch the server. They flow on a WebRTC DataChannel, which is DTLS-secured SCTP.
- Two layers of credentials. A Google ID token authenticates the HTTP layer (
/pair,/pair/claim). A short-lived session JWT (10 min TTL) authenticates the signaling WebSocket. - Identity-locked pairing. The viewer's Google
submust match the host's. Mismatches return403 identity_mismatch. - No logs of terminal content. Server keeps only the pairing record in Redis (2 min TTL) — never any frame data.
Full design write-up: security pillars on the landing.