Run gotty htop with no flags and GoTTY listens on every interface on port 8080, with no password or TLS. In our lab, a second LAN machine loaded that page with an HTTP 200 and no login. Add -w to a shell and anyone who can reach the port gets a remote shell as your user, because browser keystrokes run on the server. Settle that risk before you share a Linux terminal in a web browser with GoTTY.
GoTTY is still the fastest way to turn a command into a web page, and a few flags fix the defaults. This guide installs the maintained fork, locks it down with a password, TLS and an SSH tunnel, shares one tmux session and compares GoTTY with ttyd and tmate. The commands below were run on Debian 13 with GoTTY v1.10.0 in October 2026, and steps we did not run are labelled.
Install GoTTY from the maintained fork
Install the binary from the sorenisanerd/gotty releases page, not from the original yudai/gotty repository. The old download URL that many guides still print fetches a build from 2017, the last release of the original project, while the fork shipped its latest release in October 2026. The fork names its tarballs gotty_<tag>_linux_amd64.tar.gz, so the tag has to go into the file name as well as the path. On an ARM board, swap amd64 in the file name for the arm64 or armv7 asset listed on the releases page (we only ran the amd64 build).
GoTTY is one Go binary with nothing else to install. Detect the latest tag from the GitHub API, download the matching tarball and put the binary in /usr/local/bin. The -f flag makes curl fail loudly if the API rate-limits you, instead of leaving VER holding an error page:
cd /tmp
VER=$(curl -fsS https://api.github.com/repos/sorenisanerd/gotty/releases/latest | grep -m1 tag_name | cut -d\" -f4)
echo $VER
curl -fsSLO https://github.com/sorenisanerd/gotty/releases/download/${VER}/gotty_${VER}_linux_amd64.tar.gz
tar xzf gotty_${VER}_linux_amd64.tar.gz
sudo install -m 0755 gotty /usr/local/bin/gotty
Confirm the shell finds the new binary. If an older version string shows up, a previous copy earlier in your PATH is winning, and command -v gotty tells you where it lives.
gotty --version
A current install reports the fork’s tag, leading v included. Anything that prints a bare 1.0.1 is the old build.
gotty version v1.10.0
That 17 MB binary is the whole install. Our examples share htop and tmux, so pull both from the Debian repositories before going further.
sudo apt install -y htop tmux
Every run follows gotty [options] <command> [arguments], and gotty --help lists every option along with a GOTTY_* environment variable for each. These are the flags that decide who can see and touch your terminal:
-a,--address: listen address, default0.0.0.0-p,--port: listen port, default8080-w,--permit-write: let browsers type into the TTY, off by default-c,--credential: basic authentication asuser:pass-t,--tls: serve HTTPS with~/.gotty.crtand~/.gotty.key-r,--random-url: add an 8-character random path
Each of those gets its own section below, in the order the risk shows up. Read the warning in each one before you copy a command onto a server that faces other people, because a flag that is fine on a laptop can open a shell on a shared host.
Run a command read-only in the browser
Start with a command that cannot hurt anything, such as htop. GoTTY runs it in a pseudo-terminal and serves it to every browser that connects.
gotty htop
GoTTY prints the URL it listens on, then one alternative URL for every address on the host. We trimmed the IPv6 link-local and libvirt bridge lines from this capture, which otherwise repeat the same message for each interface.
2026/10/20 12:11:22 GoTTY is starting with command: htop
2026/10/20 12:11:22 HTTP server is listening at: http://[::]:8080/
2026/10/20 12:11:22 Alternative URL: http://127.0.0.1:8080/
2026/10/20 12:11:22 Alternative URL: http://[::1]:8080/
2026/10/20 12:11:22 Alternative URL: http://10.0.1.50:8080/
Read that list as the exposure, not a convenience. The socket table from another terminal shows GoTTY bound to every interface, and from a second host on the LAN a plain GET to http://10.0.1.50:8080/ returned 200 with no prompt for credentials:
sudo ss -tlnp | grep 8080
An asterisk in front of :8080 in the local address column means all addresses, IPv4 and IPv6. A host firewall that allows 8080, or none at all, puts the page on every network the server touches.
LISTEN 0 4096 *:8080 *:* users:(("gotty",pid=11565,fd=4))
Open any of the printed URLs and htop renders in the browser exactly as it does over SSH, with the page title set to htop@deb01 from the default {{ .command }}@{{ .hostname }} title format:

Without -w the session is view-only. We started gotty bash, typed touch /tmp/from-browser-ro into the browser and pressed Enter, and the keystrokes went nowhere: ls /tmp/from-browser-ro on the server answered “No such file or directory”. Read-only is the right default for dashboards, log tails and demos. Stop GoTTY with Ctrl+C in its terminal, or with pkill -x gotty if you started it in the background.
Allow browser input with -w only when you mean it
GoTTY’s own help marks -w “(BE CAREFUL)”, and the lab showed why. With -w on a shell, every keystroke from every connected browser runs on the server as the user who started GoTTY:
gotty -w bash
In the browser we created /tmp/from-browser-rw with touch, then ran id -un and hostname. The shell answered as josphat on deb01.

The screenshot only proves the browser saw a prompt. Back on the server, a directory listing shows whether the browser really wrote to the filesystem.
ls -l /tmp/from-browser-rw
It did. The file belongs to josphat, not to a web user or nobody, so a browser session carries the full rights of the account that started GoTTY.
-rw-rw-r-- 1 josphat josphat 0 Oct 20 12:15 /tmp/from-browser-rw
So gotty -w bash on the default listener is a passwordless shell for everyone who can route to port 8080. Never start it as root, because the browser inherits whatever that account can do. If you need write access, we would only turn it on with all of these in place:
- a credential stored in
~/.gotty - TLS with a certificate the viewer trusts
- a localhost bind reached through an SSH tunnel
- an unprivileged account that owns nothing important
The next three sections set up each of those in turn. Any one of them on its own still leaves a gap, which is why the tunnel ends up as our default.
Add a username and password
Passing -c turns on HTTP basic authentication, and it is the minimum before GoTTY listens anywhere other than localhost. Start htop behind a credential.
gotty -c admin:S3cretPass htop
We checked it with curl from the same host, then listed the process. No credentials and wrong credentials both get 401, the right pair gets 200, and the challenge header names the realm GoTTY.

Look at the last line of that capture, because it is the problem. A password passed with -c sits in the process arguments, so ps -o args= -C gotty prints gotty -c admin:S3cretPass htop to every local user on the machine. On a shared server, move the credential into the ~/.gotty config file shown further down. Basic auth also sends the password base64-encoded on every request, which is readable to anyone on the path unless TLS is on as well.
A random path with -r is the other option people reach for. In the lab GoTTY served /fu74gimq/, the root path returned 404 and the random path returned 200. That hides the page from casual scans, but the URL travels in chat messages, browser history and proxy logs, so treat it as a speed bump on top of -c, never as a replacement for it.
Enable TLS on the GoTTY session
With -t, GoTTY serves HTTPS using a certificate and key from your home directory, unless you point --tls-crt and --tls-key at other files. Those files do not exist on a fresh install, so the first try usually fails.
Error: open /home/josphat/.gotty.crt: no such file or directory
GoTTY does not create a certificate for you. Running gotty -t htop before you make one prints its https:// listening URLs (trimmed here to the first one), then exits:
2026/10/20 12:13:54 TLS crt file: /home/josphat/.gotty.crt
2026/10/20 12:13:54 TLS key file: /home/josphat/.gotty.key
2026/10/20 12:13:54 HTTP server is listening at: https://[::]:8080/
Error: open /home/josphat/.gotty.crt: no such file or directory
Ignore that “listening” line, because nothing is listening by the time you read it, and pgrep -x gotty came back empty for us. For a lab or an internal box, a self-signed certificate is enough to get going. Set the CN and SAN to the name your viewers will type, so the browser warning at least names the right host:
openssl req -x509 -newkey rsa:2048 -nodes -days 365 -keyout ~/.gotty.key -out ~/.gotty.crt -subj "/CN=deb01.example.com" -addext "subjectAltName=DNS:deb01.example.com,IP:127.0.0.1"
chmod 600 ~/.gotty.key
ls -l ~/.gotty.crt ~/.gotty.key
Only your user should be able to read the key afterwards. The certificate can stay world-readable, since it holds nothing secret.
-rw-rw-r-- 1 josphat josphat 1184 Oct 20 12:13 /home/josphat/.gotty.crt
-rw------- 1 josphat josphat 1704 Oct 20 12:13 /home/josphat/.gotty.key
Start GoTTY with TLS and the credential together, since TLS without a password only encrypts an open door:
gotty -t -c admin:S3cretPass htop
Four curl requests from the server show how each kind of client is treated. They run from plain HTTP up to a client that trusts the certificate, which is the case a real viewer should end up in.
curl -s -o /dev/null -w "http on tls port: %{http_code}\n" http://127.0.0.1:8080/
curl -sk -o /dev/null -w "https: %{http_code}\n" -u admin:S3cretPass https://127.0.0.1:8080/
curl -s -o /dev/null -w "https no -k: %{http_code}\n" -u admin:S3cretPass https://127.0.0.1:8080/; echo rc=$?
curl -s --cacert ~/.gotty.crt -o /dev/null -w "https with cacert: %{http_code}\n" -u admin:S3cretPass https://127.0.0.1:8080/
Plain HTTP to the TLS port gets 400, and HTTPS with -k gets 200, so you know the TLS listener and the credential both work. Without -k, curl refuses the self-signed certificate with exit code 60, and passing the certificate itself as the CA brings it back to 200:
http on tls port: 400
https: 200
https no -k: 000
rc=60
https with cacert: 200
Browsers behave like curl without -k: each viewer gets a certificate warning and has to accept it, which trains people to click through warnings. For anything beyond your own lab, give viewers a certificate their browser already trusts. The usual way is a reverse proxy with a Let’s Encrypt certificate in front of a localhost-bound GoTTY, as in our Nginx setup guide. We did not test a proxy in front of GoTTY for this article, so treat that as a pointer rather than a tested recipe.
Keep GoTTY on localhost and tunnel over SSH
When the person you are sharing with already has SSH access, skip certificates and passwords entirely. Bind GoTTY to the loopback address and let SSH carry the traffic, so nothing new listens on the network at all:
gotty -a 127.0.0.1 -p 8080 htop
Now the log prints a single URL, and the socket table shows the loopback address instead of an asterisk. Nothing on the LAN can open a connection to it.
2026/10/20 12:18:40 GoTTY is starting with command: htop
2026/10/20 12:18:40 HTTP server is listening at: http://127.0.0.1:8080/
LISTEN 0 4096 127.0.0.1:8080 0.0.0.0:* users:(("gotty",pid=12024,fd=4))
A request from another host to port 8080 now fails to connect, which is what you want. On the viewer’s workstation, open a background tunnel from local port 18080 to the server’s loopback port 8080:
ssh -f -N -L 18080:127.0.0.1:8080 [email protected]
That is the exact tunnel command we ran. Then browse to http://127.0.0.1:18080/ on your workstation. In the lab that request returned 200 through the tunnel while the direct request kept failing. This is the pattern we’d pick for one-off sharing: SSH already authenticates and encrypts, and there’s no certificate to manage. A loopback bind only keeps out the network, though. Every local user on the server can still reach 127.0.0.1:8080, so on a shared host keep the credential in ~/.gotty even behind the tunnel. Our guide to creating SSH tunnels on Linux covers the -L options in more depth.
A --once run looks like a good companion here, but check what it does first. Its help text says “Accept only one client and exit on disconnection”, yet in our test a plain HTTP GET left GoTTY running afterwards. We did not test a full browser session against it, so stop GoTTY yourself when the share is over instead of relying on --once.
Move GoTTY options into ~/.gotty
GoTTY loads ~/.gotty on every start if the file exists, and that is where the credential belongs instead of the command line. Create the file in your home directory. Our Debian 13 cloud image ships full vim, but a minimal install may only have vim-tiny, so use nano or run sudo apt install vim first:
vim ~/.gotty
We ran exactly this file: HTTPS on port 19000, basic auth on, write access off. Change the port and the credential to your own values:
port = "19000"
enable_tls = true
enable_basic_auth = true
credential = "admin:S3cretPass"
permit_write = false
once = false
A password sits in that file, so make it private to your account before you start GoTTY. The chmod is a standard permission step we added afterwards. Our lab run started GoTTY with the file at its default mode:
chmod 600 ~/.gotty
gotty htop
GoTTY logs that it loaded the file and switched authentication on, with no flags on the command line. Because the credential is no longer an argument, ps has nothing to show your other local users.
2026/10/20 12:14:04 Loading config file at: /home/josphat/.gotty
2026/10/20 12:14:04 GoTTY is starting with command: htop
2026/10/20 12:14:04 Using Basic Authentication
An HTTPS request to port 19000 with the credential returned 200. TLS still uses ~/.gotty.crt and ~/.gotty.key, so create them first or GoTTY exits with the same certificate error as above. Remember the file applies to every gotty you start under that account, so remove it or pass --config with another path when you want different defaults for a one-off share.
Run one shared tmux session for several viewers
GoTTY starts a new copy of the command for every browser that connects. Five viewers of gotty bash get five separate shells, not one shared screen. To show everyone the same session, have GoTTY run tmux with new -A, which creates the session for the first client and attaches every later client to it:
gotty tmux new -A -s shared
You drive the session from your own SSH login by sending keys into it. Keys only land once a session exists, so either wait for the first viewer or create the session yourself before starting GoTTY. We tested the second order: tmux new -d -s shared over SSH, then the gotty command above, and keys sent with tmux send-keys showed up in the browser. Attaching interactively with tmux attach -t shared should also work, but we did not test it.
error connecting to /tmp/tmux-1000/default (No such file or directory)
We sent keys to the session right after starting GoTTY and got this error three times, once for each tmux call, and after a reset the same attempt reported no server running on /tmp/tmux-1000/default. Both mean the same thing: no tmux server exists yet, because GoTTY only runs tmux new -A -s shared when the first browser connects. Create the session yourself with tmux new -d -s shared or open the GoTTY URL in a browser first, then send keys from the SSH session:
tmux ls
tmux send-keys -t shared "echo typed-over-ssh-into-the-shared-session; uptime -p" Enter
tmux capture-pane -pt shared | grep -v "^$" | tail -3
Run that before any session exists and you get the error above. With a viewer connected, tmux ls shows the session as attached, and the pane holds the command’s output:
shared: 1 windows (created Tue Oct 20 12:16:27 2026) (attached)
typed-over-ssh-into-the-shared-session
up 41 minutes
josphat@deb01:~$
In the browser, the same lines appeared live above the tmux status bar labelled [shared]. Every extra viewer runs the same tmux new -A and lands in that one session.

Viewers stay read-only because you started GoTTY without -w, while you type freely from SSH. That is the setup we’d use for teaching or a live incident review. Our tmux guide covers sessions, windows and key bindings if tmux is new to you, and when you want a replayable record rather than a live view, see how to record terminal sessions on Linux.
GoTTY vs ttyd vs tmate
Pick GoTTY when you want the most options and a config file, ttyd when you want the same job in a far smaller process, and tmate when your viewer cannot reach your server at all. We ran GoTTY and ttyd side by side on the same Debian 13 VM, and only looked at the tmate package without running it:
| Item | GoTTY (sorenisanerd fork) | ttyd | tmate |
|---|---|---|---|
| Debian 13 package | No, GitHub release tarball | No, static binary from GitHub | Yes, apt install tmate (2.4.0-3) |
| Binary size | 17 MB | 1.3 MB | Not measured |
| Idle RSS in our lab | 13,824 KB | 1,868 KB | Not measured |
| Browser input | Off, enable with -w | Off, enable with -W | Not tested |
| Basic auth flag | -c user:pass | -c user:pass | Not tested |
| Where the credential leaks | Process list | Process list and startup log (base64) | Not tested |
| Traffic path | Direct to your server | Direct to your server | Relayed through tmate.io servers by default |
Memory is the row that surprised us. ttyd idled at about a seventh of GoTTY’s resident memory, which matters on a small VPS or a Raspberry Pi class board and not at all on a workstation. Both tools cap clients, check the WebSocket origin and offer a one-shot mode, since ttyd has -m, -O and -o for those. What GoTTY adds is the ~/.gotty config file and --timeout.
ttyd is not in the Debian 13 repositories, so apt-cache policy ttyd prints nothing (Debian packages it only in forky and sid). Grab the static binary from the ttyd releases page:
cd /tmp
curl -fsSL -o ttyd https://github.com/tsl0922/ttyd/releases/latest/download/ttyd.x86_64
sudo install -m 0755 ttyd /usr/local/bin/ttyd
ttyd --version
Our lab downloaded the pinned 1.7.7 asset of the same name, while the latest/download URL above fetches whatever is current. The binary is 1.3 MB against GoTTY’s 17 MB. Our lab build reported its version with the commit hash appended:
ttyd version 1.7.7-40e79c7
ttyd takes the same -c credential syntax and uses -W for write access. Start a shell behind a password the same way as with GoTTY:
ttyd -p 7681 -c admin:S3cretPass -W bash
ttyd answered 401 without credentials and 200 with them, but its startup log prints the credential, base64-encoded:
[2026/10/20 12:19:23:3127] N: ttyd 1.7.7-40e79c7 (libwebsockets 4.3.3-unknown)
[2026/10/20 12:19:23:3128] N: tty configuration:
[2026/10/20 12:19:23:3128] N: credential: YWRtaW46UzNjcmV0UGFzcw==
[2026/10/20 12:19:23:3128] N: start command: bash
[2026/10/20 12:19:23:3128] N: close signal: SIGHUP (1)
[2026/10/20 12:19:23:3128] N: terminal type: xterm-256color
Base64 is an encoding, not encryption, and that string decodes straight back to admin:S3cretPass. If ttyd runs under systemd, its journal would keep that password for anyone who can read the logs, so restrict log access or front it with the same SSH tunnel. Our ttyd walkthrough goes further into its options, and WeTTY is the other browser terminal we cover.
tmate is a different design, a tmux fork that shares the session through tmate.io’s relay servers by default, so the viewer never connects to your server directly, at the price of routing your terminal through a third party. We only checked that Debian 13 ships it as a package, so we haven’t verified its behaviour here. For sharing from a server you control, our pick is GoTTY bound to localhost behind an SSH tunnel, with a ~/.gotty file holding the password when you need a direct listener.