A full mosh client that runs entirely in your browser.
Pure JavaScript. No server-side terminal, no agents, nothing to install on the remote host. Falls back to a built-in SSH-2 client when mosh isn't available.
This project was born on the Shinkansen between Tokyo and Kyoto, watching an SSH session die every few minutes as the train hopped between cell towers and tunnels at 285 km/h.
SSH from a phone or laptop is miserable: switch from Wi-Fi to LTE, close the lid, walk out of range — your session is gone. mosh fixed this in 2012 with a UDP state-sync protocol that survives roaming and sleep, but it has always required a native client — and on an iPhone, "a native client" means whatever the App Store happens to offer.
moshweb implements the entire mosh protocol in browser JavaScript — the OCB3-AES128 AEAD cipher, the State Synchronization Protocol datagram layer, the protobuf-based screen/keystroke sync, all of it. Open a tab, connect, put your phone in your pocket, and your session is still there an hour later.
- 🖥️ Real mosh, not a lookalike — interoperates with stock
mosh-server. SSP datagrams, OCB3-AES128 (RFC 7253, verified against the RFC test vectors), hand-rolled protobuf, zlib, ACK/retransmit/heartbeat. - 🔐 Built-in SSH-2 client for bootstrapping and fallback — curve25519-sha256 key exchange, aes128-ctr + hmac-sha2-256, ed25519 public-key / password / keyboard-interactive auth, host-key pinning (TOFU). All crypto is pure JS (noble), so it even works on plain
http://origins where WebCrypto is unavailable. - 🔄 Automatic fallback chain: mosh → ssh → ssh+tmux. If
mosh-serveris missing or UDP is blocked, you still get a shell, and tmux keeps it alive across reconnects. - 📱 Made for phones:
- Touch scrolling that does the right thing — wheel events to tmux/vim when the app owns the mouse, arrow keys on alternate screens, local scrollback otherwise
- A key bar (esc / tab / ctrl / arrows) above the soft keyboard, with sticky-Ctrl
- Layout that tracks the iOS visual viewport, so the input line is never hidden behind the keyboard
- 🇯🇵 First-class CJK & IME support — Unicode 11 width tables (wide chars and emoji measured correctly), native browser IME composition, deduplication for iOS Safari's double-fired IME commits, CJK font stack,
mosh-serverlaunched with a UTF-8 locale. - 🔌 Survives almost anything (see resilience) — WebSocket drops, full bridge restarts, network roaming, laptop sleep.
- 🧪 Tested against the real thing — Node interop tests against genuine
mosh-server/sshd, plus browser E2E tests that restart the bridge mid-session and verify Japanese round-trips.
| Connect | On a phone |
|---|---|
![]() |
![]() |
Browsers can't open raw UDP or TCP sockets, so a ~250-line Node.js process relays bytes between a WebSocket and the network. That's the only thing that runs outside the browser. It never sees keys or plaintext — mosh datagrams are end-to-end encrypted from the JS client to mosh-server, and the SSH transport is likewise encrypted in the browser.
The connection flow:
- The built-in SSH client connects and runs
mosh-server new - The reply
MOSH CONNECT <port> <key>seeds the mosh engine, which takes over on UDP; the bootstrap SSH connection closes - If anything in step 1–2 fails (no mosh-server, UDP filtered), you transparently land in an SSH PTY shell — optionally inside
tmux new -Aso the session itself becomes durable
git clone https://github.com/hisa110/moshweb && cd moshweb
npm install
npm run build # bundle the browser app -> public/app.js
npm start # start the bridge -> open http://127.0.0.1:8022/Fill in host / user / auth (password or an OpenSSH ed25519 private key) and connect. Done.
📖 The recommended deployment — bridge on a home server, phone connecting over Tailscale — has its own step-by-step guide: docs/setup.md. It covers the slightly unusual topology (the "host" field is relative to the bridge!), systemd, tmux mouse mode, and troubleshooting.
The bridge binds loopback only by default. To reach it over a VPN, list the addresses explicitly:
MOSHWEB_HOST=127.0.0.1,$(tailscale ip -4) node bridge.js
# then open http://<machine>.your-tailnet.ts.net:8022/ on your phoneBecause the crypto is pure JS, everything works on plain http:// — no certificate setup required. If you want HTTPS anyway, tailscale serve --bg 8022 gives you a trusted cert in one command.
⚠️ Never bind0.0.0.0. The bridge is a TCP/UDP relay; exposing it to an untrusted network lets anyone on it relay traffic to arbitrary hosts. Bind loopback + VPN addresses only.
Already have a running mosh-server? Open 詳細設定 / Advanced, paste <port> <key> from its MOSH CONNECT line, and moshweb connects over UDP without touching SSH.
The browser ⇔ bridge WebSocket is treated as unreliable by design:
| Failure | What happens |
|---|---|
| WebSocket drops (mosh) | Auto-reconnect. SSP is stateless over the wire — nothing is lost |
| WebSocket drops (ssh) | Both directions are buffered with byte offsets + ACKs and replayed on reattach — not a single byte lost |
| Bridge process restarts | mosh: a fresh UDP pipe is created, same session continues. ssh: auto re-dial with kept credentials (tmux restores your screen) |
| Wi-Fi → LTE, sleep, dead zones | Classic mosh behavior: the screen syncs the instant connectivity returns |
- Unicode 11 width tables via
@xterm/addon-unicode11— full-width characters and emoji occupy the correct number of cells - IME composition uses the browser's native composition events; the in-progress text renders at the cursor
- iOS Safari fires some IME commits twice through different event paths — moshweb deduplicates identical non-ASCII chunks arriving within 50 ms
- Keystrokes travel as UTF-8 inside SSP
UserStreammessages, exactly like native mosh mosh-serveris started withLANG=ja_JP.UTF-8(falling back toC.UTF-8)
- Bridge: binds
127.0.0.1(plus explicitly listed VPN addresses), rejects cross-origin WebSockets, relays ciphertext only - SSH host keys: trust-on-first-use, pinned in
localStorage, connection refused if the key changes - Credentials: used in-memory for the session; only host/user/options are persisted, never passwords or keys
- mosh key: the AES key from
MOSH CONNECTlives only in the page's memory
| Path | What it is |
|---|---|
src/mosh/ocb.js |
OCB3 AES-128 AEAD (RFC 7253) — passes the RFC test vectors |
src/mosh/ssp.js |
mosh datagram layer: nonces, direction bit, timestamps, fragmentation, zlib |
src/mosh/sync.js |
state synchronization: UserStream diffs, ACKs, retransmit, heartbeat, shutdown |
src/mosh/pb.js |
minimal hand-written protobuf for mosh's Transport/Client/HostBuffers messages |
src/ssh/ |
complete SSH-2 client: transport, kex, auth, channels, PTY |
src/term.js |
xterm.js setup: CJK fonts, Unicode 11, touch scrolling, viewport tracking |
src/relay.js |
reconnecting WebSocket session with byte-offset replay |
bridge.js |
the local WS ↔ UDP/TCP relay |
node test/ssp-node.mjs <port> <key> # mosh protocol interop against a real mosh-server
node test/ssh-node.mjs <keyfile> # SSH interop against a local sshd
node test/e2e-mosh.mjs <port> <key> # browser E2E, direct mosh
node test/e2e-full.mjs # full E2E: ssh→mosh, bridge restart, fallback, 日本語- Predictive local echo (mosh's speculative typing) — not yet implemented
- Encrypted (passphrase-protected) private keys
- RSA / ECDSA client keys (ed25519 only today; host keys support all three)
- Connection profiles for one-tap access to multiple servers
- PWA packaging for a home-screen app feel
PRs welcome — see CONTRIBUTING.md.
- mosh by Keith Winstein and contributors — the protocol this project reimplements for the browser
- xterm.js — the terminal emulator
- noble cryptography — auditable pure-JS crypto primitives
If moshweb keeps your session alive on a train, consider leaving a ⭐

