Run the ~130-line bridge on the phone itself with Termux, then open this page in the phone browser and point it at ws://127.0.0.1:8080/ssh. Terminal output, scrollback and profiles live in the app's RAM; Termux holds only the tiny relay.
1 · install Node in Termux
pkg update && pkg install nodejs-lts
mkdir -p ~/pocketsh && cd ~/pocketsh
npm init -y && npm i ws ssh2
2 · save bridge.js (see the bridge.js tab)
nano bridge.js # paste, Ctrl+O, Enter, Ctrl+X
3 · run it
BRIDGE_TOKEN=change-me node bridge.js
termux-wake-lock # keeps it alive when the screen sleeps
Notes
- Termux runs in a separate sandbox — the browser reaching
127.0.0.1 works because it is the same device loopback.
- Keep
BIND=127.0.0.1 so nothing is exposed to the local Wi-Fi.
- Add the page to your home screen for an app-like terminal (fullscreen, no chrome).
Native Node on Windows, or inside WSL. Nothing else to install — no Cygwin, no PuTTY needed for the browser session.
PowerShell
winget install OpenJS.NodeJS.LTS
mkdir $HOME\pocketsh; cd $HOME\pocketsh
npm init -y; npm i ws ssh2
# save bridge.js here, then:
$env:BRIDGE_TOKEN="change-me"; node bridge.js
Or in WSL
sudo apt install nodejs npm && mkdir -p ~/pocketsh && cd ~/pocketsh
npm i ws ssh2 && BRIDGE_TOKEN=change-me node bridge.js
Notes
- Windows Firewall may prompt once for Node listening on 8080 — allow it for private networks only if you bind beyond loopback.
- To keep it running after logout:
pm2 start bridge.js --name pocketsh.
Recommended: run the bridge on the VPS itself. Then no process runs on your phone or laptop at all — the browser connects straight to wss://bridge.yourdomain.com/ssh and the bridge dials localhost:22.
Install & run
sudo apt install nodejs npm
mkdir -p /opt/pocketsh && cd /opt/pocketsh
npm init -y && npm i ws ssh2
# save bridge.js, then a service:
sudo tee /etc/systemd/system/pocketsh-bridge.service <<'EOF'
[Unit]
Description=PocketSSH WebSocket-SSH bridge
After=network.target
[Service]
Environment=BIND=127.0.0.1
Environment=PORT=8080
Environment=BRIDGE_TOKEN=change-me
ExecStart=/usr/bin/node /opt/pocketsh/bridge.js
Restart=always
User=pocketsh
[Install]
WantedBy=multi-user.target
EOF
sudo useradd -r -s /usr/sbin/nologin pocketsh
sudo systemctl enable --now pocketsh-bridge
TLS in front (Caddy handles WebSocket upgrade)
bridge.yourdomain.com {
reverse_proxy /ssh* 127.0.0.1:8080
}
Notes
- Keep the bridge bound to loopback and let the proxy do TLS — never expose plain
ws:// to the internet.
- Prefer key auth; the bridge is a credential conduit, so treat it like a bastion host.
- Add
--allow-remote-hosts-style policy by hard-coding the target host in your own fork if you don't want users choosing destinations.
Short answer: the web platform has no raw TCP. A page can only speak HTTP, WebSocket and WebRTC, so something server-side must translate WebSocket ⇄ SSH.
What the bridge does
- Accepts one WebSocket per terminal session, checks a shared token.
- Dials the VPS with
ssh2, verifies the host key fingerprint (TOFU by default, pinnable).
- Opens a PTY shell and pipes bytes both ways as base64 in small JSON frames.
- Relays resize events and answers latency pings.
The true no-bridge path
- Chrome's Direct Sockets API gives pages
TCPSocket/UDPSocket, and the docs literally cite "a web-based SSH client" as a use case.
- But it is gated behind Isolated Web Apps: manifest
permissions_policy with direct-sockets, cross-origin isolation, and an installed, signed app bundle.
- Availability is Chrome/ChromeOS desktop class only — not Android Chrome, not Safari, not Firefox. You would still need an SSH2 implementation compiled to WebAssembly on top of it.
Prior art