https://github.com/0xshawn/remote-shell
Browser-based shell with transparent tmux-backed session persistence — refresh, close the tab, or switch devices and resume the exact same shell (cwd, env, running processes).
https://github.com/0xshawn/remote-shell
docker node-pty nodejs persistent-sessions pty remote-shell shell terminal tmux ttyd web-shell web-terminal websocket xterm-js
Last synced: about 10 hours ago
JSON representation
Browser-based shell with transparent tmux-backed session persistence — refresh, close the tab, or switch devices and resume the exact same shell (cwd, env, running processes).
- Host: GitHub
- URL: https://github.com/0xshawn/remote-shell
- Owner: 0xshawn
- License: mit
- Created: 2026-06-18T20:28:50.000Z (28 days ago)
- Default Branch: main
- Last Pushed: 2026-07-01T17:15:59.000Z (15 days ago)
- Last Synced: 2026-07-01T17:26:06.464Z (15 days ago)
- Topics: docker, node-pty, nodejs, persistent-sessions, pty, remote-shell, shell, terminal, tmux, ttyd, web-shell, web-terminal, websocket, xterm-js
- Language: Java
- Size: 824 KB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# remote-shell
A monorepo for **remote-shell**, a browser-based interactive shell whose defining
feature is **session persistence**: refresh, close the tab, or switch devices and
you resume the *exact* same shell — working directory, environment, running
processes, and history all intact.
The server forks a PTY directly and keeps a per-session in-memory **ring buffer**;
the frontend (xterm.js + WebGL, 50k-line scrollback) owns all scrolling, so it
stays buttery-smooth and a reconnect repaints the screen from the buffer. No tmux.
```
Browser (xterm.js + WebGL) ──WebSocket──► Go gateway ──fork──► PTY ──► bash/zsh (or `ssh host`)
│
per-session ring buffer → reconnect repaints
```
## Projects
| Path | Project | Description |
|------|---------|-------------|
| [`server/`](server/) | **server** | Go gateway: PTY + ring-buffer session persistence, WebSocket wire protocol, auth. See [`server/README.md`](server/README.md). |
| [`web/`](web/) | **web** | xterm.js + WebGL frontend (no build step; vendored addons). Served by the Go server. |
| [`android/`](android/) | **android** | Native Android client (Kotlin + Jetpack Compose, Termux terminal emulator) that speaks the same login API and WebSocket protocol. See [`android/README.md`](android/README.md). |
| [`deploy/`](deploy/) | **deploy** | Docker Compose, Dockerfile, and the nginx TLS proxy. Driven by [`install.sh`](install.sh). |
## Quick start
**Server + web** (Docker) — one command, no clone needed:
```bash
curl -fsSL https://raw.githubusercontent.com/0xshawn/remote-shell/main/install.sh | bash
# open the printed https://:8443 (self-signed cert → accept the warning)
```
`install.sh` fetches the repo into `~/.remote-shell` (override with
`REMOTE_SHELL_DIR=`) when run outside a checkout, then builds the image, creates
`.env`, auto-detects your host user, generates + persists the secrets, generates
a self-signed TLS cert, and authorizes the container's SSH key on the host —
then prints the login password. It is safe to re-run. From an existing clone,
run `./install.sh` directly.
By default the web terminal logs into the **host** shell over SSH (not the
container). To pin credentials, use real TLS certs, or switch to a plain
container shell, see [`server/README.md`](server/README.md).
**Binary** (no Docker) — download one self-contained binary and run it in the
background; it serves its own HTTPS:
```bash
curl -fsSL https://raw.githubusercontent.com/0xshawn/remote-shell/main/install-binary.sh | bash
# open the printed https://:8443 (self-signed cert → accept the warning)
```
`install-binary.sh` downloads the prebuilt binary for your architecture, runs it
under **systemd** when available (else a `nohup` process that survives the shell
closing), and prints the generated password. The web terminal is the host user's
own shell — no SSH hop. Serve on a **different port** with `PORT=` (e.g.
`PORT=9443 bash remote-shell-installer-linux-amd64.sh`); pin a version with
`REMOTE_SHELL_VERSION=`. See [`server/README.md`](server/README.md) for changing
the port of an existing install, management, and uninstall.
**Offline / blocked network** (the server can't reach GitHub) — grab the
**self-extracting installer** `remote-shell-installer-linux-amd64.sh` (or `-arm64`)
from the [releases page](https://github.com/0xshawn/remote-shell/releases). It's
this script with the binary baked in, so one file is the whole package — copy it
over and run it; no network, no separate binary, no checkout:
```bash
bash remote-shell-installer-linux-amd64.sh
```
Prefer the raw binary? Download `remote-shell-linux-amd64`, then
`./install-binary.sh ./remote-shell-linux-amd64` (or set `REMOTE_SHELL_BIN=`).
Run it directly instead (Go ≥ 1.26):
```bash
cd server && go build -o remote-shell . && cd ..
WEB_DIR=web ./server/remote-shell --no-auth
```
**Android client:**
```bash
cd android
./gradlew assembleDebug
```
Then point the app at your server's URL and log in with the same credentials.
## Uninstall
Each installer has an `uninstall` subcommand that tears down its own deployment.
**Docker** — removes the containers, the volume (secrets + host SSH key), the
image, and the authorized host key:
```bash
~/.remote-shell/install.sh uninstall # from the checkout (default ~/.remote-shell)
```
**Binary** — removes the systemd service (system or user), the binary, and
`~/.remote-shell` (password, token secret, self-signed cert):
```bash
remote-shell uninstall # the binary cleans up after itself (add -y to skip the prompt)
# or, if the binary is already gone: ./install-binary.sh uninstall
```
Both are best-effort and safe to re-run. For a **system** (root) install, run them
with `sudo` so root-owned units/binaries can be removed.
Manual teardown (binary), if you'd rather do it by hand
`install-binary.sh` installs a **system** service (as root), a **user** service
(non-root), or a bare **nohup** process. `sudo systemctl` only sees the system one
— "Unit could not be found" usually means a user service or nohup. Detect, then remove:
```bash
systemctl --user status remote-shell ; pgrep -af remote-shell # which one is it?
# stop the matching one:
sudo systemctl disable --now remote-shell && sudo rm -f /etc/systemd/system/remote-shell.service
systemctl --user disable --now remote-shell && rm -f ~/.config/systemd/user/remote-shell.service
kill "$(cat ~/.remote-shell/remote-shell.pid)" 2>/dev/null
# then:
rm -f /usr/local/bin/remote-shell ~/.local/bin/remote-shell
rm -rf ~/.remote-shell
```
(If `systemctl --user` reports "Failed to connect to bus" over SSH, run
`export XDG_RUNTIME_DIR=/run/user/$(id -u)` first.)