> your AI agent picks dependencies from memory; give it dated facts — try starlog.dev ↗ vet your agent's deps ↗ vibe-coding is fine. vibe-importing isn’t. — try starlog.dev ↗ vibe-importing isn’t fine ↗ your agent has never seen your private packages — try starlog.dev ↗ facts for private packages ↗ a linter for the dependencies your AI agent picks — try starlog.dev ↗ a linter for agent deps ↗

Back to Articles

GoTTY: Turn Any CLI Tool Into a Web App With One Command

[ View on GitHub ]

GoTTY: Turn Any CLI Tool Into a Web App With One Command

Hook

With a single command—gotty top—you can share your system monitor with anyone who has a web browser, no SSH keys or terminal emulators required. It's terminal sharing distilled to its absolute essence.

Context

Remote terminal access has traditionally meant SSH, VPNs, or screen-sharing software. But what if you want to share a long-running build process with a colleague? Demo a CLI tool to a prospect who doesn't have a terminal? Let support staff view logs without granting server access? The conventional solutions are overkill: SSH requires key management and client software, VPNs demand network configuration, and screen sharing wastes bandwidth showing your entire desktop.

GoTTY emerged from this friction. Written by Iwasaki Yudai in Go, it bridges the gap between terminal applications and web browsers using a remarkably simple architecture: spawn a command in a pseudo-TTY, capture its I/O, and stream it to web clients via WebSockets. The browser handles rendering with xterm.js, a full-featured terminal emulator written in TypeScript. The result is a single binary that turns any CLI tool into a web application instantly, no configuration required.

Technical Insight

GoTTY's elegance lies in its straightforward architecture. When you execute gotty <command>, the server starts an HTTP listener, spawns your command in a pseudo-terminal (PTY), and waits for WebSocket connections. Each connected browser establishes a bidirectional WebSocket channel—stdout/stderr flow to the client for rendering, while stdin (if permitted) flows back to the process.

Here's the most basic usage:

# Share a read-only view of system processes
gotty -p 8080 top

# Allow interactive shell access (dangerous!)
gotty -p 8080 --permit-write /bin/bash

# Generate a random URL path for ad-hoc sharing
gotty -p 8080 --random-url --permit-write tmux new -A -s shared

The PTY abstraction is crucial. Unlike simply capturing stdout/stderr, a PTY makes the spawned process believe it's running in a real terminal. This means ncurses applications, color codes, cursor positioning, and terminal resizing all work correctly. When a browser client changes dimensions, GoTTY sends SIGWINCH to the process, triggering proper reflow—exactly as a native terminal would.

The server-side code flow is approximately: (1) Accept WebSocket connection, (2) Fork the configured command with a new PTY, (3) Proxy PTY output to WebSocket frames, (4) If --permit-write, proxy WebSocket input to PTY stdin, (5) Handle disconnection and process cleanup. Each connection spawns a new process instance by default, which has interesting implications for resource usage and state isolation.

For production deployments, GoTTY supports extensive configuration. Consider this setup for a secure, authenticated monitoring dashboard:

# Generate a self-signed certificate (use real certs in production)
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
  -keyout key.pem -out cert.pem

# Launch with TLS and basic auth
gotty -p 8080 \
  --tls \
  --tls-crt cert.pem \
  --tls-key key.pem \
  --credential admin:secretpassword \
  --permit-write=false \
  --reconnect \
  htop

The --reconnect flag deserves attention. By default, closing the browser tab kills the underlying process. With reconnection enabled and a terminal multiplexer like tmux, you get persistent sessions that survive disconnects:

# Shared, persistent session that multiple users can reconnect to
gotty -p 8080 \
  --permit-write \
  --reconnect \
  --reconnect-time 60 \
  tmux new-session -A -s collab bash

This spawns a single tmux session. All connected clients share the same terminal state—true collaborative editing or troubleshooting. Without tmux, each client gets an isolated process instance, which is usually what you want for tools like top or one-off commands.

GoTTY's configuration flexibility extends to a JSON config file, useful for complex deployments:

{
  "address": "0.0.0.0",
  "port": "8080",
  "permit_write": false,
  "enable_basic_auth": true,
  "credential": "user:pass",
  "enable_tls": true,
  "tls_crt_file": "/path/to/cert.pem",
  "tls_key_file": "/path/to/key.pem",
  "title_format": "GoTTY - {{ .Command }} ({{ .Hostname }})",
  "enable_reconnect": true,
  "reconnect_time": 60,
  "max_connection": 10,
  "once": false
}

The --once flag creates ephemeral sessions—the server exits after the first client disconnects. Perfect for generating temporary access links or CI/CD integration where you want self-cleaning resources.

Under the hood, GoTTY uses the github.com/kr/pty package for PTY allocation and github.com/gorilla/websocket for WebSocket handling. The frontend leverages xterm.js (with an option for the older hterm), which handles VT100/xterm escape sequences, mouse events, and clipboard integration. The WebSocket protocol is straightforward: binary frames for terminal output, text frames for input, with JSON messages for metadata like window resizing.

Gotcha

The elephant in the room: GoTTY appears largely unmaintained. The last significant commit was years ago, and several open issues report compatibility problems with modern terminals and browsers. Dependencies are outdated, security patches lag, and the project hasn't adapted to newer xterm.js APIs. For hobby projects or internal tools, this might be acceptable. For production systems handling sensitive data or requiring long-term support, it's a red flag.

Security requires careful attention. The --permit-write flag essentially grants remote code execution—anyone with access can run arbitrary commands as the GoTTY process user. Even read-only mode can leak sensitive information if you're streaming logs or system information. Default settings use unencrypted HTTP, transmitting everything in plaintext. You must explicitly enable TLS, authentication, and consider network-level restrictions (firewalls, VPNs) for anything beyond local development. The random URL feature (--random-url) provides obscurity, not security—anyone who discovers the URL gains access. Resource exhaustion is another concern without connection limits: each client spawns a process (absent tmux), and malicious actors could spawn hundreds of shells. The --max-connection flag mitigates this but isn't foolproof. Finally, terminal emulation isn't perfect—some applications with complex escape sequences or unusual terminal requirements may render incorrectly or crash.

Verdict

Use if: You need quick, ad-hoc terminal sharing for demos, remote support, or making CLI tools accessible to non-technical users. It excels at "show me what's happening" scenarios—monitoring long-running builds, streaming logs, demonstrating DevOps tools, or providing read-only observability dashboards. The zero-configuration startup and single-binary distribution make it unbeatable for rapid deployment. It's also excellent for internal tools where the maintenance status matters less than immediate functionality.

Skip if: You need active maintenance and security updates for production systems. The project's dormancy makes it risky for critical infrastructure or long-term deployments. Avoid it for high-security environments unless you're prepared to fork and maintain it yourself, or consider actively-maintained alternatives like ttyd. Skip it if you need complex terminal features, sophisticated session management, or guaranteed compatibility with modern web standards—you'll spend more time debugging than you save in deployment simplicity. For serious collaborative terminal work, purpose-built tools like tmate or VS Code's remote development extensions offer better experiences.