Instant Terminal Sharing
Upterm is an open-source tool enabling developers to share terminal sessions securely over the web. It’s perfect for remote pair programming, accessing computers behind NATs/firewalls, remote debugging, and more.
This is a blog post to describe Upterm in depth.
brew install --cask owenthereal/upterm/upterm
If you previously installed upterm using the Homebrew formula (without --cask), you'll need to migrate to the Cask version:
# Uninstall the old formula version
brew uninstall upterm
# Install the new Cask version
brew install --cask owenthereal/upterm/upterm
Note: Running brew upgrade with the old formula installed will fail with an error. Follow the migration steps above to resolve this.
scoop bucket add upterm https://github.com/owenthereal/scoop-upterm
scoop install upterm
upterm can be easily installed as an executable. Download the latest compiled binaries and put it in your executable path.
go install github.com/owenthereal/upterm/cmd/upterm@latest
git clone https://github.com/owenthereal/upterm.git
cd upterm
go install ./cmd/upterm/...
Host starts a terminal session:
upterm host
Host retrieves and shares the SSH connection string:
upterm session current
Client connects using the shared string:
ssh [email protected]
Dive into more commands and advanced usage in the documentation. Below are some notable highlights:
Host a session with any desired command:
upterm host -- docker run --rm -ti ubuntu bash
Host a session with specified client public key(s) authorized to connect:
upterm host --authorized-keys PATH_TO_PUBLIC_KEY
Or authorize users by provider:username, fetching their public keys from a
code-hosting service:
upterm host --authorized-user github:username
upterm host --authorized-user gitlab:username
upterm host --authorized-user srht:username
upterm host --authorized-user codeberg:username
Self-hosted instances are supported by naming the host. gitea and forgejo
always require one, since there is no default instance. Keys are always fetched
over HTTPS:
upterm host --authorized-user github:[email protected]
upterm host --authorized-user gitea:[email protected]
upterm host --authorized-user forgejo:[email protected]
upterm host --authorized-user https://git.example.com/username
For a GitHub Enterprise Server instance that requires a login, authenticate
first with gh auth login --hostname ghe.example.com; only credentials stored
for that host are used.
Host a session initiating tmux new -t pair-programming, while ensuring clients join with tmux attach -t pair-programming.
This mirrors functionality provided by tmate:
upterm host --force-command 'tmux attach -t pair-programming' -- tmux new -t pair-programming
Host a session from a script or CI step with nothing attached to its terminal. --accept skips the confirmation prompt, --name gives the session a local name you choose, and --pty-size pins the terminal geometry so the command renders the same for every client:
upterm host --accept --name build-shell --pty-size 132x43 -- bash &
In a fresh environment known_hosts does not yet hold the relay's key, and the host-key confirmation cannot be answered without a terminal. Add it first, or pass --skip-host-key-check to accept an unknown key on the first connection:
mkdir -p ~/.ssh && ssh-keyscan uptermd.upterm.dev >> ~/.ssh/known_hosts
Look the session up by name while it runs and after it ends. The record outlives the process and carries how the command finished:
upterm session info build-shell -o json
The status field is starting, ready, disconnected or ending while the session still holds its name, and ended once nobody does; reason is exited (with exitCode), signaled, stopped, startup_failed, startup_abandoned (declined at the confirmation prompt) or unknown. The hosted command sees its own name in UPTERM_SESSION_NAME. upterm session list shows every live session, including one started under a different XDG_RUNTIME_DIR — a cron job or a system service — reached through the admin socket path its record carries. Records outlive the sessions that wrote them for seven days, and the listing prunes the ones past that.
Clients can transfer files using standard scp or sftp commands. The connection details are shown when running upterm session current:
# Download a file from host
scp -P PORT USER@HOST:/path/to/file.txt ./local/
# Upload a file to host
scp -P PORT ./local/file.txt USER@HOST:/path/to/destination/
Security model:
--accept, each file operation prompts the host for approval via a dialog--read-only to restrict SFTP to downloads only (no uploads, deletes, or modifications)--no-sftp to disable file transfers entirelyClients can use standard SSH local forwarding through a hosted session when the host opts in:
upterm host --allow-local-tcp-forwarding
ssh -L 5555:127.0.0.1:8080 [email protected]
In scenarios where your host restricts ssh transport, establish a connection to uptermd.upterm.dev (or your self-hosted server) via WebSocket:
upterm host --server wss://uptermd.upterm.dev -- bash
Clients can connect to the host session via WebSocket as well:
ssh -o ProxyCommand='upterm proxy wss://[email protected]' [email protected]:443
If the host can only reach the internet through an HTTP proxy, pass it with --proxy. It works with ssh://, ws:// and wss:// servers, so the default server works too as long as the proxy allows CONNECT to port 22:
upterm host --proxy http://proxy.example.com:3128 -- bash
Without --proxy, ws:// and wss:// connections already use HTTPS_PROXY/HTTP_PROXY, but ssh:// connections go direct. Many corporate proxies only allow CONNECT to port 443; in that case, use --server wss://uptermd.upterm.dev as well.
Like other flags, --proxy can be set with UPTERM_PROXY or as proxy in the config file, which keeps proxy credentials off the command line. Clients behind a proxy pass the same flag to upterm proxy.
upterm can be integrated with GitHub Actions to enable real-time SSH debugging, allowing you to interact directly with the runner system during workflow execution. This is achieved through action-upterm, which sets up an upterm session within your CI pipeline.
To get started, include action-upterm in your GitHub Actions workflow as follows:
name: CI
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup upterm session
uses: owenthereal/action-upterm@v1
This setup allows you to SSH into the workflow runner whenever you need to troubleshoot or inspect the execution environment. Find the SSH connection string in the Checks tab of your Pull Request or in the workflow logs.
For comprehensive details on configuring and using this integration, visit the action-upterm GitHub repo.
Issue: The command upterm session current does not display the current session when used within Tmux.
Cause: This occurs because upterm session current requires the UPTERM_ADMIN_SOCKET environment variable, which is set in the specified command. Tmux, however, does not carry over environment variables not on its default list to any Tmux session unless instructed to do so (Reference).
Solution: To rectify this, add the following line to your ~/.tmux.conf:
set-option -ga update-environment " UPTERM_ADMIN_SOCKET UPTERM_SESSION_NAME"
Issue: It might be unclear whether your shell command is running in an upterm session, especially with common shell commands like bash or zsh.
Solution: Use upterm session current -o go-template to customize your shell prompt with session info. Add to your ~/.bashrc or ~/.zshrc:
# Show emoji and connected client count when in upterm session
export PS1='$(upterm session current -o go-template=" {{.ClientCount}} " 2>/dev/null)'"$PS1"
Template variables available (Go templates use PascalCase field names):
{{.SessionID}} - Session ID{{.ClientCount}} - Number of connected clients{{.Host}} - Server host{{.Command}} - Command being shared{{.ForceCommand}} - Force command (if set)Note: JSON output (
-o json) uses camelCase keys (e.g.,sessionId,clientCount).Tip: The same template mechanism can be used for terminal titles or other integrations.
Alternative (simpler, without client count):
export PS1="$([[ ! -z "${UPTERM_ADMIN_SOCKET}" ]] && echo -e '\xF0\x9F\x86\x99 ')$PS1"
Upterm starts an SSH server (a.k.a. sshd) in the host machine and sets up a reverse SSH tunnel to a Upterm server (a.k.a. uptermd).
Clients connect to a terminal session over the public internet via uptermd using ssh or ssh over WebSocket.
You can deploy uptermd to a Kubernetes cluster. Install it with helm:
helm repo add upterm https://upterm.dev
helm repo update
helm install uptermd upterm/uptermd
The cheapest way to deploy a worry-free Upterm server (a.k.a. uptermd) is to use Fly.io.
Fly offers a generous free tier and excellent global performance. The official uptermd community server is hosted on Fly.
Install the Fly CLI and authenticate:
curl -L https://fly.io/install.sh | sh
flyctl auth login
Copy fly.example.toml to fly.toml and set your app
name. It pulls the published ghcr.io/owenthereal/upterm/uptermd image, so
no local build is needed.
Deploy your uptermd server:
flyctl deploy
Your uptermd server will be available at your-app-name.fly.dev. You can connect using either SSH or WebSocket protocols.
Upgrading from an earlier release:
uptermd-flyno longer exists. Replace your[build] dockerfile/build-targetand[experimental] entrypointsettings with the[build] imageand[env]blocks shown infly.example.toml.
uptermd expands environment variable references in its text configuration
values — flags, UPTERMD_* environment variables, and config files alike. This
exists because the container image has no shell, so values that need a runtime
value (a machine ID, a pod IP) cannot be interpolated before the process starts.
Expansion applies to text value
No open issues yet, or sync has not completed.