Terminal sharing for IT professionals. Share your terminal over SSH and the web — built on tmux 3.6a.
# Install
curl -fsSL https://tmtv.se/install.sh | sh
# Start sharing
tmtvThat's it. tmtv connects to tmtv.se and prints your session tokens:
ssh TOKEN@tmtv.se # read-write access
ssh ro-TOKEN@tmtv.se # read-only access
SSH sharing is always on. Web sharing is opt-in:
tmtv set -g tmtv-web-sharing on
# [tmtv] web session: https://tmtv.se/s/TOKENShare the URL — viewers watch in their browser, no install needed.
To auto-enable web sharing, add to ~/.tmtv.conf:
set -g tmtv-web-sharing on
By default, web viewers can only watch. To let RW web viewers type:
tmtv set -g tmtv-web-input onWhen enabled, web viewers connected via the RW token can type in the terminal. RO token viewers remain view-only. A badge in the viewer titlebar shows the session mode ("interactive" or "view-only").
Pick a memorable name instead of a random token:
# In ~/.tmtv.conf
set -g tmtv-session-name demo
Your session is now at:
ssh 12345-demo@tmtv.se(SSH read-write, random prefix)ssh ro-demo@tmtv.se(SSH read-only)https://tmtv.se/s/demo(web)
Names must be 3-32 characters, alphanumeric and hyphens only.
Require a password for viewers to join:
tmtv -p secretSSH viewers must authenticate with the password — public key authentication alone is rejected. Web viewers see a password prompt before the terminal loads. Passwords are SHA-256 hashed server-side and never stored in plain text.
Can also be set in config:
# In ~/.tmtv.conf
set -g tmtv-session-password secret
Record your terminal session to an asciinema .cast file:
tmtv set -g tmtv-recording onRecordings are saved to ~/.tmtv/recordings/. Play them back with asciinema play:
asciinema play ~/.tmtv/recordings/demo-1741737600.castAdd a toggle keybinding to start/stop recording with prefix + R:
# In ~/.tmtv.conf
bind R if -F '#{==:#{tmtv-recording},1}' \
'set -g tmtv-recording off; display "Recording stopped"' \
'set -g tmtv-recording on; display "Recording started"'
Set a time-to-live for your session:
# In ~/.tmtv.conf
set -g tmtv-link-ttl 3600 # expire after 1 hour
The session auto-terminates after the TTL. Viewers see the countdown in the session notification.
Here's every sharing option in one place:
# ~/.tmtv.conf
# Web sharing (off by default, SSH is always on)
set -g tmtv-web-sharing on
# Allow RW web viewers to type (off by default)
set -g tmtv-web-input on
# Named session — gives you a memorable URL
set -g tmtv-session-name demo
# Password — required for all viewers (SSH and web)
set -g tmtv-session-password secret
# Shared clipboard (on by default) — RO viewers excluded
set -g tmtv-shared-clipboard on
# Record terminal to asciinema .cast file
set -g tmtv-recording on
# Session lifetime in seconds (0 = no limit)
set -g tmtv-link-ttl 3600
All options can be combined freely. A named session with password and web input gives you:
ssh 12345-demo@tmtv.se— SSH read-write (password required)ssh ro-demo@tmtv.se— SSH read-only (password required)https://tmtv.se/j/demo— web viewer with typing enabled (password prompt shown)
Options can also be toggled at runtime:
tmtv set -g tmtv-web-sharing on # enable web sharing mid-session
tmtv set -g tmtv-web-input off # disable web input mid-session
tmtv set -g tmtv-shared-clipboard off # disable clipboard sharing mid-sessionDebug CI failures by SSH-ing into your runner. Drop-in replacement for action-tmate:
- name: Debug via tmtv
if: failure()
uses: sa3lej/action-tmtv@v1
with:
limit-access-to-actor: trueMigrating from action-tmate? One-line diff:
- uses: mxschmitt/action-tmate@v3
+ uses: sa3lej/action-tmtv@v1See action-tmtv for full docs.
flowchart LR
subgraph Host
A["tmtv client<br/>(terminal + tmux 3.6a)"]
end
subgraph Server["tmtv-server (relay)"]
B["SSH relay<br/>+ auth"]
C["SSE stream<br/>(port 4002)"]
end
subgraph Viewers
D["SSH viewer<br/>(read-write)"]
E["SSH viewer<br/>(read-only)"]
F["Browser<br/>(xterm.js)"]
end
A -- "SSH tunnel" --> B
B -- "SSH" --> D
B -- "SSH" --> E
C -- "HTTP/SSE" --> F
style A fill:#0c0c16,stroke:#6c9efc,color:#6c9efc
style B fill:#0c0c16,stroke:#6c9efc,color:#6c9efc
style C fill:#0c0c16,stroke:#c084fc,color:#c084fc
style D fill:#0c0c16,stroke:#3ddc84,color:#3ddc84
style E fill:#0c0c16,stroke:#3ddc84,color:#3ddc84
style F fill:#0c0c16,stroke:#c084fc,color:#c084fc
The host runs tmtv, which opens an SSH tunnel to the relay server. The server authenticates viewers and forwards the terminal stream — read-write or read-only over SSH, view-only over SSE to browsers. Web viewers see exactly what SSH viewers see, rendered by xterm.js.
- Full tmux 3.6a terminal multiplexer
- Instant session sharing over SSH with read-write and read-only tokens
- Password-protected sessions — viewers must authenticate before connecting
- Shared clipboard — copy on host, paste on viewers (and vice versa)
- Web viewer with xterm.js and WebGL rendering
- Interactive web input — RW web viewers can type when enabled by the host
- Late-join support — browser viewers see current terminal state
- Server-Sent Events streaming — works through proxies and firewalls
- Live viewer counts —
S:N W:Nin tmux status bar and web titlebar - Dynamic page titles — link previews show the session name (Slack, Discord, iMessage)
- SIXEL graphics support — images display in terminal and stream to viewers
- Session recording to asciinema
.castformat - Per-user input API for multiplayer terminal apps
- Read-only viewers can disconnect cleanly with Ctrl-Q
- Automatic reconnection with exponential backoff on server disconnect
- Zero config — just type
tmtv
Session URLs are available as tmux format variables:
| Variable | Description |
|---|---|
#{tmtv_ssh} |
SSH connection string (read-write) |
#{tmtv_ssh_ro} |
SSH connection string (read-only) |
#{tmtv_web} |
Web viewer URL |
#{tmtv_session_name} |
Named session name (if set) |
#{tmtv_ssh_viewers} |
Number of connected SSH viewers |
#{tmtv_web_viewers} |
Number of connected web viewers |
Example: tmtv display-message -p '#{tmtv_web}'
tmtv exposes a Unix socket (TMTV_INPUT_SOCKET) that programs running inside a session can connect to for per-user input events. Each viewer gets a unique identity, enabling multiplayer terminal games, in-session chat, and collaborative tools where you need to know who typed what.
Programs can subscribe to viewer join/leave notifications, receive per-user keystrokes, and control whether keys are mirrored to the terminal or captured exclusively.
See docs/input-socket.md for the wire protocol, message types, and examples.
tmtv prepends live viewer counts to the standard tmux status bar. The default status-right is:
S:#{tmtv_ssh_viewers} W:#{tmtv_web_viewers} "#{=21:pane_title}" %H:%M %d-%b-%y
This gives you something like:
S:2 W:1 "myhost" 14:30 09-Mar-26
Everything after the viewer counts is the stock tmux default — pane title, 12-hour clock, and date. This is set via the tmux options system, so you can override it in ~/.tmtv.conf like any tmux option.
Add to ~/.tmtv.conf:
set -g status-right 'S:#{tmtv_ssh_viewers} W:#{tmtv_web_viewers} "#{=21:pane_title}" %H:%M %Y-%m-%d'
This keeps the viewer counts and pane title, switches to a 24-hour clock (%H:%M), and uses ISO date format (%Y-%m-%d).
Any valid tmux status-right format works — tmtv is a full tmux, so you have access to all tmux format variables alongside the tmtv-specific ones. See man tmux for the complete list of format strings.
tmtv is a full tmux replacement. All tmux commands work:
tmtv new-session -s work
tmtv split-window -h
tmtv list-sessions
tmtv attach -t workRun your own tmtv relay server instead of using tmtv.se.
docker compose up -dThat's it — SSH sharing on port 2222. Clients connect via ssh TOKEN@your.host.com -p 2222.
For production with TLS and web viewing, add Caddy as a reverse proxy:
TMTV_DOMAIN=share.example.com docker compose --profile tls up -dThis starts both tmtv-server and a Caddy instance with automatic TLS. SSH host keys and TLS certificates persist in Docker volumes.
| Variable | Description | Default |
|---|---|---|
TMTV_DOMAIN |
Hostname in connection strings and TLS cert | localhost |
TMTV_SSH_PORT |
SSH listen port (host-side) | 2222 |
TMTV_IDLE_TIMEOUT |
Disconnect idle sessions (seconds) | 3600 |
TMTV_MAX_LIFETIME |
Maximum session lifetime (seconds) | 86400 |
TMTV_LOG_LEVEL |
Log verbosity (0=quiet, 1+=verbose) | 0 |
flowchart LR
subgraph Docker
S["tmtv-server<br/>:2222 SSH<br/>:4002 SSE"]
C["Caddy<br/>:80/:443"]
end
Client -- "SSH :2222" --> S
Browser -- "HTTPS :443" --> C
C -- "/ws/* (SSE proxy)" --> S
C -- "/healthz" --> S
style S fill:#0c0c16,stroke:#6c9efc,color:#6c9efc
style C fill:#0c0c16,stroke:#3ddc84,color:#3ddc84
The Caddy profile is optional — without it, only SSH sharing is available. The SSE port (4002) is internal only; Caddy proxies it at /ws/*.
sudo apt-get install build-essential autoconf automake pkg-config \
libevent-dev libncurses-dev libssh-dev libmsgpack-dev libbsd-dev \
bison libutf8proc-dev
sh autogen.sh
./configure --enable-sixel
make -j$(nproc)This produces tmtv (client) and tmtv-server.
brew install autoconf automake pkg-config libevent libssh msgpack-c bison utf8proc
export PATH="$(brew --prefix bison)/bin:$PATH"
sh autogen.sh
./configure --enable-utf8proc --enable-sixel
make -j$(sysctl -n hw.ncpu)Build fully static musl binaries for any architecture:
docker build --build-arg PLATFORM=amd64 --output type=local,dest=./out .
# Produces: out/build/tmtv and out/build/tmtv-server (zero dependencies)Supported platforms: amd64, arm64, arm/v7, arm/v6, i386.
mkdir -p /etc/tmtv/keys
ssh-keygen -t ed25519 -f /etc/tmtv/keys/ssh_host_ed25519_key -N ''
ssh-keygen -t rsa -b 3072 -f /etc/tmtv/keys/ssh_host_rsa_key -N ''sudo cp tmtv-server /usr/local/bin/# /etc/systemd/system/tmtv-server.service
[Unit]
Description=tmtv relay server
After=network.target
[Service]
ExecStart=/usr/local/bin/tmtv-server \
-k /etc/tmtv/keys \
-p 22 \
-h your.host.com \
-v
Restart=always
NoNewPrivileges=yes
ProtectSystem=strict
ReadWritePaths=/tmp
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable --now tmtv-serverThat's it — SSH sharing works now. Clients connect via ssh TOKEN@your.host.com.
Web sharing requires an SSE endpoint and a reverse proxy with TLS. Add -z 4002 to the server flags to enable SSE streaming, then put Caddy in front for TLS and routing.
A production-ready Caddyfile is provided in deploy/Caddyfile. It includes Caddy templates for dynamic page titles (link previews show the session name in Slack, Discord, iMessage). Replace tmtv.se with your hostname.
Build and deploy the web viewer:
cd site && npm ci && npm run build && cd ..
sudo mkdir -p /var/www/tmtv
sudo cp site/dist/index.html /var/www/tmtv/
sudo cp site/dist/viewer/index.html /var/www/tmtv/viewer.html
sudo cp web/viewer.js /var/www/tmtv/Open these ports:
| Port | Protocol | Purpose |
|---|---|---|
| 22 | TCP | tmtv SSH (clients and viewers) |
| 443 | TCP | HTTPS (web viewer and landing page) |
Port 4002 (SSE) should NOT be exposed — Caddy handles it internally.
| Flag | Description | Default |
|---|---|---|
-k |
SSH host keys directory | keys |
-p |
SSH listen port | 2222 |
-q |
SSH port shown in connection strings | same as -p |
-h |
Hostname in connection strings | system hostname |
-z |
SSE port for web viewer | disabled |
-b |
Bind address | all interfaces |
-A |
Require authorized keys (reject clients without -a) |
off |
-x |
Enable PROXY protocol (for use behind load balancers) | off |
-V |
Print version and exit | — |
-v |
Increase log verbosity | quiet |
tmtv-server uses libssh (>= 0.9.5) with modern defaults:
- Key exchange: curve25519-sha256, ecdh-sha2-nistp256/384/521
- Ciphers: chacha20-poly1305, aes256-gcm, aes128-gcm, aes256-ctr, aes128-ctr
- Host keys: Ed25519, ECDSA, RSA (SHA-256/SHA-512 signatures only)
- No weak algorithms: no 3DES, no DH group1/group14-sha1, no ssh-rsa with SHA-1
# ~/.tmtv.conf
set -g tmtv-server-host your.host.com
set -g tmtv-server-port 22
set -g tmtv-server-rsa-fingerprint SHA256:...
set -g tmtv-server-ed25519-fingerprint SHA256:...
Get fingerprints:
ssh-keygen -lf /etc/tmtv/keys/ssh_host_ed25519_key.pub
ssh-keygen -lf /etc/tmtv/keys/ssh_host_rsa_key.pubRun locally — no server required:
cd tests && make allRun against a live tmtv-server (180+ tests covering SSH, SSE, web viewer, pane operations, viewer counts, web input, SIXEL, clipboard, flood resilience, and more).
Tests run ON the test server in local mode:
# Quick mode (skips slow tests)
TEST_HOST=localhost WEB_HOST=staging.tmtv.se sh test-integration.sh --quick
# Full suite
TEST_HOST=localhost WEB_HOST=staging.tmtv.se sh test-integration.shOr use the deploy script:
scripts/staging-deploy.sh --test-only # quick mode
scripts/staging-deploy.sh --test-only --full-tests # full suiteISC license, same as tmux.
If tmtv is useful to you, buy me a coffee.