Skip to content

wsh

@johnhenry/wsh (“Web Shell”) is a browser-native remote command execution client: open PTY and exec sessions on a remote host from a web page or Node.js, authenticated with Ed25519 keys, over either WebTransport or WebSocket with an identical API. It speaks a compact CBOR wire protocol with 97 message types, and includes file transfer (list, read, write, rename, upload, download), session recording (its own JSON schema), a remote-MCP bridge, and reverse (peer-accept) mode.

The package root is the client and stays browser-safe. A separate subpath, @johnhenry/wsh/server, is a Node host for the same protocol: WebSocket and optional WebTransport listeners, with exec, pty, fs, MCP tools, resumable sessions, a pinnable host key, password auth and a relay all opt-in. See Node server.

Two things distinguish the current releases. The WebSocket transport multiplexes with QMux (draft-ietf-quic-qmux-02) — real QUIC-v1 frames with windowed flow control and backpressure, not an ad-hoc mux — and the primitives are exported so alternate servers can speak the same framing. And the security story runs deeper than the handshake: auth signs a transcript that binds the username, reverse-mode peer registrations are self-signed and verifiable, session traffic can be sealed end-to-end with a hybrid post-quantum (X25519+ML-KEM-768) key exchange feeding real AES-256-GCM frame encryption (session.enableE2E(sharedSecret, { role }) — see the guide), and host identity can be pinned: the Node server advertises a signed host key, and the client checks it with expectHostKey or a WshKnownHosts trust-on-first-use store.

A parallel Rust implementation (crates/: wsh-core, wsh-client, wsh-cli, wsh-server) speaks the same wire protocol and ships a wsh CLI binary (wsh connect/scp/sftp/ls/reverse/agent/copy-id). It is not feature-identical to the Node server: the Rust wsh-server advertises no host key, refuses file write and rename, and replays its whole output ring on resume regardless of last_seq. The repo README’s capability matrix lists every difference; this site documents the JS client and the Node server.

Previously published as wsh-upon-star (last release 0.1.1, now deprecated), in the repo johnhenry/wsh-upon-star. Renamed to @johnhenry/wsh on import into the @johnhenry family. Unlike most of the family it did not restart its version numbers on adoption: it was already a mature release, and the numbering continues forward (0.23.0 at this writing). Since the rename it has reworked auth, the mux, file transfer, session resumption, and E2E frame encryption, and gained a Node server, host-key pinning and a relay.

Terminal window
npm install @johnhenry/wsh

Latest on npm: @johnhenry/[email protected].

Requires Node.js 26+ (the package’s engines floor) or a browser with Web Crypto Ed25519 support (roughly Safari 17+, Chrome/Edge 137+, Firefox 130+). WebTransport is optional: it needs roughly Chrome/Edge 97+, Firefox 114+ or Safari 26+, and the WebSocket transport works everywhere and presents the same API. The library has zero required runtime dependencies. For the hybrid post-quantum key exchange it prefers native WebCrypto ML-KEM-768 (Node 24.7+) and falls back to the optional @noble/post-quantum dependency where native support is absent; no browser tested has native ML-KEM yet, so on the web the fallback is what runs.

@johnhenry/wsh/server additionally needs the optional peer ws (npm install ws), and its WebTransport listener the optional peers @fails-components/webtransport and @fails-components/webtransport-transport-http3-quiche.

The single most important thing to know: you choose the transport, the rest of your code doesn’t change. WebTransport gives you native multiplexed streams; WebSocket multiplexes QMux streams over one connection. Same client, same sessions, same methods — only the transport option differs.

import { WshClient, generateKeyPair } from '@johnhenry/wsh';
const keyPair = await generateKeyPair(true);
const client = new WshClient();
await client.connect('wss://shell.example.com', {
username: 'alice',
keyPair,
transport: 'ws', // or 'webtransport' — nothing else changes
});
  • Guide — connect, PTY sessions, one-shot exec, detach/resume, files, host-key pinning, keys
  • Node server — host the protocol with @johnhenry/wsh/server: exec, pty, fs, MCP tools, sessions, WebTransport, relay
  • Tutorial — handshake to exec over real wire frames, no network
  • API — every export, grouped

Source: github.com/johnhenry/wsh · Runnable examples in examples/ — each named for the behavior it proves (frames surviving a fragmented transport, a tampered challenge failing auth, …). The protocol is specified machine-readably in the repo’s spec/wsh-v1.yaml, from which the message constants are code-generated — the wire format is a contract, not folklore.