README
¶
zotty
zotty (zero-overhead-tty) is a small terminal bridge for the zot web UI (web.zot.sh) or similar. It runs on the same machine as your zot host and gives the web UI a real shell in a pseudo-terminal. zot itself is not involved or changed.
Security: zotty grants full shell access as the user running it. Anyone who can reach it and has the token can run any command. Keep it on localhost and expose it only through a private tunnel such as Tailscale. Do not publish it through a public tunnel.
Install
One-liner (macOS, Linux)
curl -fsSL https://raw.githubusercontent.com/patriceckhart/zotty/main/install.sh | bash
Detects your OS and architecture, downloads the latest release from GitHub, verifies the SHA-256 against the release's checksums.txt, and installs the binary into /usr/local/bin, ~/.local/bin, or ~/bin, whichever is writable first. Pass a version or prefix to pin:
curl -fsSL https://raw.githubusercontent.com/patriceckhart/zotty/main/install.sh | bash -s -- v0.0.1 ~/bin
Windows is not supported, because zotty needs a Unix pseudo-terminal.
go install
go install github.com/patriceckhart/zotty@latest
From source
go build -o zotty .
Check the installed version with zotty version.
Updates
zotty update --check # show whether a newer release exists
zotty update # download, verify, and install it
zotty update replaces the running binary in place after verifying the release checksum. Restart zotty serve afterwards. A root-owned install (for example in /usr/local/bin) needs sudo. Development builds are not updated.
On start, zotty serve checks in the background for a newer release and logs a notice if there is one. The result is cached for 12 hours in the zotty config directory, and the check never delays or blocks startup. Set ZOTTY_NO_UPDATE_CHECK=1 to turn it off.
Run
zotty serve
On first start it creates a random token in your user config directory (macOS: ~/Library/Application Support/zotty/token, mode 0600) and prints it. In the web UI, open the terminal button in a conversation's top bar and enter the bridge URL and this token.
Flags
| Flag | Default | Description |
|---|---|---|
--listen |
127.0.0.1:7789 |
Address to listen on. A non-loopback address logs a warning. |
--token-file |
user config dir | Token file, created if missing. |
--origins |
https://web.zot.sh |
Comma-separated browser origins allowed in addition to localhost. |
--shell |
$SHELL, then /bin/sh |
Shell to start as a login shell. |
--dir |
home directory | Working directory for new shells. |
--upstream |
off | Forward every path except /term and /health to this address, for example zot's --web listener 127.0.0.1:7787. Lets one tunnel serve zot and zotty. |
Remote access with Tailscale
tailscale serve --bg --https=8443 http://127.0.0.1:7789
Then use https://<machine>.<tailnet>.ts.net:8443 as the terminal URL.
Remote access with cloudflared
Warning: a cloudflared tunnel is reachable from the public internet. Only the token protects the shell. Prefer Tailscale when you can.
Quick tunnel for zot and zotty (no Cloudflare account, no domain):
zot continuous serve --store ~/.zot-store --web 127.0.0.1:7787
zotty serve --upstream 127.0.0.1:7787
cloudflared tunnel --url http://127.0.0.1:7789
zotty serves /term and forwards everything else to zot. With the printed https://<random>.trycloudflare.com address, use:
- host URL:
https://<random>.trycloudflare.com - terminal URL:
https://<random>.trycloudflare.com/term
The address changes on every start. Both endpoints keep their own token: the zot web token and the zotty token.
Named tunnel (Cloudflare account and your own domain): route zot and zotty through one hostname by path:
ingress:
- hostname: zot.example.com
path: ^/term
service: http://127.0.0.1:7789
- hostname: zot.example.com
service: http://127.0.0.1:7787
- service: http_status:404
Then use https://zot.example.com as the host URL and https://zot.example.com/term as the terminal URL.
Behavior
- One shell per connection. Closing the tab or reconnecting ends that shell (use
tmuxinside it for persistent sessions). - At most 8 shells at a time.
- The token is sent in the first WebSocket message, not in the URL, so it does not appear in tunnel or proxy logs.
- Browser origins other than localhost and
--originsare rejected. - Unix only (macOS, Linux).
Protocol
WebSocket at /term:
| Direction | Frame | Content |
|---|---|---|
| client to server | text, first | {"type":"auth","token":"...","cols":80,"rows":24} |
| client to server | text | {"type":"resize","cols":120,"rows":40} |
| client to server | binary | terminal input |
| server to client | text | {"type":"ready"} after authentication |
| server to client | text | {"type":"exit","code":0} when the shell exits |
| server to client | binary | terminal output |
A wrong token closes the connection with status 1008, too many shells with 1013.
Test
go test -race ./...
License
MIT
Documentation
¶
Overview ¶
Command zotty (zero-overhead-tty) is a small terminal bridge for the zot web UI.
It serves a WebSocket endpoint that starts a login shell in a pseudo-terminal for each authenticated connection. It is independent of zot itself and grants full shell access as the user running it, so it listens on localhost by default and requires a token.