CLI tool
owenthereal/upterm avatar
owenthereal/upterm

Upterm shares a terminal over SSH without asking you to open a port

Instant Terminal Sharing

1,326 stars87 forksGoApache-2.0

At a glance

What is it?
Upterm is a Go tool that lets a developer share a live terminal session with someone else over the web, using a hosted relay so neither side needs an inbound connection, with per-session key authorisation and support for hardware keys. It fits remote pair programming and debugging a machine behind NAT, and it does not fit a workflow that needs an audit trail or a self-hosted data path.
Who is it for?
Upterm fits a developer who needs to hand a colleague a live shell on a machine behind NAT, or to debug a remote box without opening a port, and it does not fit a team that requires the traffic to stay inside its own network or that needs a record of who did what.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository received new commits within the last day.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The host, the session string and the relay are the whole model

Upterm is an open-source tool enabling developers to share terminal sessions securely over the web, and the README names the use cases as remote pair programming, accessing computers behind NATs and firewalls, remote debugging, and more.

The basic flow is three commands. The host starts a session with upterm host. The host retrieves and shares the connection string with upterm session current. The client joins using the shared string with upterm join ssh://[email protected].

The important architectural detail is that the client does not need to reach the host. A hosted relay, uptermd.upterm.dev, carries the connection, so the host's machine can be behind NAT with no inbound port, and the client needs no infrastructure either. The README also shows that plain ssh works with no install at all:

console
ssh [email protected]

That last line is what makes Upterm usable with people who will not install anything. The session is an SSH session, so a colleague with a stock terminal can join a session they know nothing about.

The repository confirms the shape of the implementation. There is a host/ directory, a join/ directory, a server/ directory for the relay, a routing/ directory, a ws/ directory for the websocket transport, an internal/ directory, and separate cmd/upterm and cmd/uptermd entry points. The project is Apache-2.0 licensed, and the last push was on 2026-10-15 with v0.38.0 released on 2026-10-13.

Access control is per session, and there is an open mode you have to notice

The access control story is the part of Upterm worth reading carefully, because the default is permissive.

You can restrict a session to specific client public keys with upterm host --authorized-keys PATH_TO_PUBLIC_KEY. You can also authorise users by provider:username, where the public key is fetched from a code hosting service, and the providers listed are github, gitlab, srht and codeberg. Self-hosted instances are supported by naming the host, and gitea and forgejo always require one since there is no default instance. Keys are always fetched over HTTPS, and for a GitHub Enterprise Server instance that requires a login you authenticate first with gh auth login --hostname ghe.example.com, where only credentials stored for that host are used.

The behaviour with no restriction is the part to notice. A session with no --authorized-keys and no --authorized-user accepts any key, but not none. Started with --accept as well, such a session is open, meaning anyone with the session ID can join and nobody is asked. upterm host warns about it as the session starts, and upterm session info says so, with "open": true in its JSON.

That warning is the whole security model in one line. The session ID is the credential, and a session ID that is pasted into a chat window is a session anyone with that chat can join.

Upterm authenticates with your SSH identity, and the guest uses a session key

The SSH key model has two layers, and understanding them explains the hardware key behaviour.

upterm host authenticates to the server with your SSH identity when the tunnel is established, the same as ssh would, and again whenever it reconnects. Everything else, meaning guest joins, upterm attach and key renegotiation, uses a key generated for the session. So an agent that confirms each signature, such as gpg-agent with a smartcard, 1Password or a FIDO key, asks at start.

The reconnect behaviour then depends on the server's configuration. On a server that accepts any host, as uptermd.upterm.dev does, a reconnect uses the session's own key and does not ask the agent, unless --private-key names the identities, in which case those are then the only ones offered, on every reconnect too. On a server with --authorized-keys, such an agent asks on every reconnect. The README's advice for a long-lived session that will reconnect is to use a key file or an agent without per-use confirmation.

To keep such an agent out of it entirely, you name a plain key, and a supplied --private-key is the whole set, like OpenSSH's IdentitiesOnly:

console
ssh-keygen -t ed25519 -N '' -f ~/.ssh/upterm
upterm host --private-key ~/.ssh/upterm

To use one particular agent identity, you name its public key instead, with upterm host --private-key ~/.ssh/id_ed25519_sk.pub.

Guests still authenticate with a key of their own. That separation is the design: the host's identity proves the host is who it says it is to the relay, and the session key is what the guest presents.

upterm join behaves like ssh, with a few documented differences

The join command is where the day-to-day friction lives, and the README documents its behaviour in detail.

upterm join takes the connect string from the host's Join: line. If the connection drops, it resets most terminal modes the session had set, the alternate screen and mouse reporting for example, says why it is waiting, and rejoins on its own. Enter retries at once, and Ctrl-C stops, and ~. leaves.

There is a warning attached to the force command option. upterm host --force-command starts again on each rejoin, so you should not use upterm join with a forced command whose effects must not repeat.

The key handling is offered as ssh does it, the agent's keys first, then the default key files, and it generates one only when you have none. The documented differences from ssh are worth knowing before you reach for a per-host identity file. Unlike ssh, upterm join does not read ~/.ssh/config, so per-host IdentityFile, User and ProxyJump do not apply, the target cannot be a host alias, and FIDO keys work only through the agent. The recommendation for those cases is to use -i, or plain ssh.

A forced command starts again each time upterm join rejoins, and nothing the previous one did is undone. Each guest's forced command runs on a pty of its own, with SSH_TTY, SSH_CONNECTION and SSH_CLIENT set as sshd sets them, so a program that copies over OSC 52 when it runs over SSH copies to the guest's clipboard. The --hide-client-ip option leaves out the two that carry the guest's address.

Installing is four documented routes, and the Homebrew one moved

The installation section covers macOS, Windows, standalone and Go, and the macOS route has a migration note worth reading if you used it before.

On macOS the current route is a cask:

console
brew install --cask owenthereal/upterm/upterm

If you previously installed upterm using the Homebrew formula, without --cask, you need to migrate, and the README explains why this matters. Running brew upgrade with the old formula installed will fail with an error, so the migration is: brew uninstall upterm, then brew install --cask owenthereal/upterm/upterm.

On Windows, Scoop is the route, adding a bucket and installing:

console
scoop bucket add upterm https://github.com/owenthereal/scoop-upterm
scoop install upterm

The standalone route is to download the latest compiled binaries from the releases page and put the binary in your executable path. The Go route is go install github.com/owenthereal/upterm/cmd/upterm@latest, and from source is a git clone followed by go install ./cmd/upterm/... .

The Go module path is github.com/owenthereal/upterm and it requires Go 1.26.7. The dependency list is a good map of what the tool does: charm.land/ssh for the SSH server side, creack/pty for pseudo-terminals, gorilla/websocket for the transport, spf13/cobra for the CLI, jpillora/chisel for tunnelling, hashicorp/consul/api for service discovery, prometheus/client_golang for metrics, and the charmbracelet bubbletea and bubbles libraries for the terminal UI.

The repository also carries a Dockerfile.uptermd for the relay server, a fly.toml and fly.example.toml for deployment on Fly.io, a Procfile and app.json for Heroku, a charts/ directory, terraform/ and systemd/ directories, and a memlistener/ package.

Where Upterm is the wrong tool

The first wrong-tool case is a compliance requirement around who saw what. Upterm is designed for the moment when you want to hand someone a shell, and its access model is built around a session ID and an optional key restriction. A team that needs to record a session, review it afterwards, or prove who ran which command has to build that, because the tool does not provide it.

The second is a data path requirement. The default relay is a hosted service, uptermd.upterm.dev, and traffic passes through it. The project does support self-hosting, and the repository carries the deployment files for the relay, so a team that must keep traffic inside its own network can host the server itself. But that is an additional deployment to operate, and the alternative of not exposing a shell at all is simply better for anything that is not a deliberate, short-lived collaboration.

The third is the forced command model. The README is explicit that a forced command starts again on each rejoin and nothing the previous one did is undone, which means a forced command that is not idempotent will leave a mess behind every time a guest reconnects. The tmux example is the intended pattern, where the host creates the session and clients attach to a named tmux target, but anything else needs to be written to be safe under reconnection.

The fourth is that this is a shared interactive shell, not a file transfer or a code review tool. The use case is pair programming and remote debugging, which is what it is good at. Anything that needs an audit trail, a managed credential, or a change that goes through review belongs in a different tool.

Editorial conclusion

Upterm fits a developer who needs to hand a colleague a live shell on a machine behind NAT, or to debug a remote box without opening a port, and it does not fit a team that requires the traffic to stay inside its own network or that needs a record of who did what. Before sharing a session, decide how access is controlled, because a session with neither --authorized-keys nor --authorized-user accepts any key, and a session started with --accept is open to anyone who has the session ID.

Frequently asked questions

How do I share a terminal session with Upterm?

The host runs upterm host, then retrieves the connection string with upterm session current, and the client joins with upterm join ssh://[email protected]. A plain ssh [email protected] also works, so the client needs no install.

How do I control who can join an Upterm session?

Use --authorized-keys PATH_TO_PUBLIC_KEY to restrict the session to specific client public keys, or --authorized-user with a provider and username such as github:username, where the key is fetched over HTTPS from the code hosting service. A session with neither option accepts any key, and adding --accept makes it open to anyone with the session ID.

Does Upterm work with SSH hardware keys?

Yes, through the agent. The host authenticates to the server with your SSH identity, and everything else uses a session-generated key, so an agent that confirms each signature such as a FIDO key or 1Password is asked at start. Unlike ssh, upterm join does not read ~/.ssh/config, and FIDO keys work only through the agent.

How do I install Upterm on macOS and Windows?

On macOS, brew install --cask owenthereal/upterm/upterm, and if you previously used the Homebrew formula you must uninstall it first, because brew upgrade fails with the old formula installed. On Windows, add the scoop bucket with scoop bucket add upterm https://github.com/owenthereal/scoop-upterm, then run scoop install upterm.

Official sources

  1. License: Apache-2.0
  2. owenthereal/upterm on GitHub
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/owenthereal-upterm.svg)](https://hysenlabs.com/projects/owenthereal-upterm)

Was this useful?