# shellshare > Live, read-only terminal broadcasting via a web link. One command, no signup. Site: https://shellshare.net ## Install `npx -y shellshare` (needs Node.js), or download the static binary then run `chmod +x shellshare && ./shellshare`: https://get.shellshare.net/?os=linux (also: mac, mac-arm, windows; auto-detected from the User-Agent if omitted) ## Broadcast your terminal `shellshare exec --json -- ` runs one command in a PTY, broadcasts it, and exits with the command's exit code. With `--json`, stdout is newline-delimited JSON: - First line: `{"event":"sharing","room":"","server":"https://shellshare.net","url":"https://shellshare.net/r/"}` — parse `url` to get the share link. - Last line: `{"event":"end","exit_code":N}`. - Errors: `ERROR: ...` on stderr, non-zero exit. shellshare exec --json -- npm test # share one command tail -f build.log | shellshare --json # stream a pipe (non-TTY stdin auto-detected, reads to EOF) Output is end-to-end encrypted by default; the key is the 64 hex chars after `#` in the `url` (a URL fragment the server never sees). ## Read someone's broadcast (no CLI needed) Given a link `.../r/#`: - Snapshot (history): `GET /r/.bin` → ciphertext bytes. - Live: WebSocket `/ws/v/r/` → binary frames are ciphertext records; text frames are control JSON (ignore them). Both are a stream of self-delimiting records: `[u32 BE N][12-byte nonce][ciphertext || 16-byte GCM tag]`, where `N = 12 + len(ciphertext || tag)`, AES-256-GCM, key = the 64 hex chars after `#`. (A link with no `#key` was sent with `--disable-encryption`: the bytes are then raw terminal output with no record framing — emit them directly, skip the decoder.) Inline decoder (Node, no install, nothing to download): ```js import crypto from 'node:crypto'; // Decode whole records to text; return how many bytes were consumed so a // live follower can keep a partial trailing record and resume. function decodeRecords(keyHex, buf) { const key = Buffer.from(keyHex, 'hex'); let text = '', o = 0; while (o + 4 <= buf.length) { const n = buf.readUInt32BE(o); // 12 (nonce) + ciphertext + 16 (tag) if (o + 4 + n > buf.length) break; // wait for the rest of this record const iv = buf.subarray(o + 4, o + 16); const body = buf.subarray(o + 16, o + 4 + n); // ciphertext || tag o += 4 + n; const d = crypto.createDecipheriv('aes-256-gcm', key, iv); d.setAuthTag(body.subarray(-16)); text += Buffer.concat([d.update(body.subarray(0, -16)), d.final()]).toString('utf8'); } const clean = text.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, '').replace(/\x1b\][^\x07]*\x07/g, ''); // strip ANSI return { text: clean, consumed: o }; } async function snapshot(url) { const u = new URL(url); const room = u.pathname.replace(/^\/r\//, ''); const buf = Buffer.from(await (await fetch(`${u.origin}/r/${room}.bin`)).arrayBuffer()); process.stdout.write(decodeRecords(u.hash.slice(1), buf).text); } // Live (Node >=22). seconds=0 -> until the socket closes. function follow(url, seconds = 0) { const u = new URL(url); const room = u.pathname.replace(/^\/r\//, ''); const ws = new WebSocket(`${u.protocol === 'https:' ? 'wss' : 'ws'}://${u.host}/ws/v/r/${room}`); ws.binaryType = 'arraybuffer'; let buf = Buffer.alloc(0); ws.onmessage = (e) => { if (typeof e.data === 'string') return; // control JSON: ignore buf = Buffer.concat([buf, Buffer.from(e.data)]); const { text, consumed } = decodeRecords(u.hash.slice(1), buf); process.stdout.write(text); buf = buf.subarray(consumed); }; if (seconds) setTimeout(() => ws.close(), seconds * 1000); } // node decode.mjs '' (snapshot; call follow(url) for live) snapshot(process.argv[2]); ``` Source: https://github.com/vitorbaptista/shellshare