shell-tunnel
Ultra-lightweight remote shell gateway.
A single binary that exposes command execution on a machine as a structured REST/WebSocket API — typed JSON requests and responses, resource-style sessions — so scripts, tools, and services can drive that machine programmatically.
Not a browser terminal or screen-sharing UI (cf. ttyd, gotty, wetty, sshx), not a remote-desktop product, not a general-purpose tunneling / reverse-proxy product, not a multi-user collaboration surface. The consumer is a program calling an API, not a person at a terminal.
Try it in 30 seconds
From another terminal:
# {"success":true,"exit_code":0,"output":"hello\n","duration_ms":5,"timed_out":false}
That's the whole loop: run the binary, POST a command, get structured output. No config, no account, no daemon. When you want auth, a public URL, or TLS, add one flag at a time — the next section shows each.
Install
# or a release binary (linux x64/arm64, macOS x64/arm64, windows x64)
&&
Release binaries can update themselves: shell-tunnel --update.
How it works
There is no separate server to install: the binary is the server. You run it on the machine you want to control, it listens on an HTTP port, and clients drive that machine by calling the API.
client ──HTTP/WS──▶ shell-tunnel (on the target machine) ──▶ shell
Behind NAT that port is not reachable on its own, so shell-tunnel can publish it for you —
either through a tunnel client (--tunnel) or through a relay you run yourself (--relay).
Only the server side ever needs inbound connectivity; a caller behind NAT is fine, because it
only makes outbound requests.
Reaching a machine behind NAT
The intended shape: a target you cannot reach directly, a relay with a public address, and you calling in from wherever.
target (NAT) ──dials out──▶ relay (public) ◀──you call── caller (NAT)
On the relay host — the only machine that needs an inbound port:
It generates a certificate and an enrolment token on first run and prints the exact command a device needs — including the certificate fingerprint, so nothing has to be copied:
Devices join with:
shell-tunnel --relay https://relay.example.com:8443 --enroll-token st_… --relay-fingerprint sha256:…
On the target — no port forwarding, it dials out:
From anywhere — the target is now reachable at /d/<name>:
To list what is attached without logging into any target:
curl -H "Authorization: Bearer <enroll-token>" https://relay.example.com:8443/relay/v1/devices.
No public relay of your own? shell-tunnel --tunnel --preset operator runs cloudflared and
prints a trycloudflare.com URL instead — quick to try, but Cloudflare documents it as
testing-only. See docs/USAGE.md for both paths in full.
A word on exposure
Publishing a shell means anyone holding the token can run commands as the user running
shell-tunnel. So a public path (--tunnel or --relay) turns authentication on, generates a
key if you gave none, and refuses --no-auth. Scope the token with --preset operator rather
than leaving it full-control, keep rate limiting on, and treat the URL and token as
credentials. TLS is not optional over the internet — --tls-self-signed on the relay is one
flag, and the fingerprint it prints keeps the connection authenticated, not just encrypted.
Documentation
| docs/USAGE.md | Operating guide — every flag, endpoint, and failure mode |
| docs/openapi.json | Machine-readable API contract (OpenAPI 3.0) |
Status
Implemented: sessions, one-shot and streaming execution, WebSocket, capability-scoped auth, rate limiting, public exposure (Cloudflare tunnel or self-hosted relay), in-process TLS with self-signed generation and fingerprint pinning, host-header checking, and an append-only audit trail. On the roadmap: filesystem operations and native MCP tools.
License
MIT — see LICENSE.