An open API service indexing awesome lists of open source software.

https://github.com/moukrea/sshx-mobile

Reach your computer's shell from your phone, anywhere, with zero infrastructure — a touch-native sshx terminal app + persistent host tooling
https://github.com/moukrea/sshx-mobile

android mobile remote-shell self-hosted sshx terminal tmux zero-infrastructure

Last synced: 9 days ago
JSON representation

Reach your computer's shell from your phone, anywhere, with zero infrastructure — a touch-native sshx terminal app + persistent host tooling

Awesome Lists containing this project

README

          

# sshx-mobile

**Reach your computer's shell from your phone — anywhere, with zero infrastructure.**

[![Release](https://img.shields.io/github/v/release/moukrea/sshx-mobile?sort=semver)](https://github.com/moukrea/sshx-mobile/releases/latest)
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](./LICENSE)
[![CI](https://github.com/moukrea/sshx-mobile/actions/workflows/pr.yml/badge.svg)](https://github.com/moukrea/sshx-mobile/actions)

sshx-mobile turns an [sshx.io](https://sshx.io) session into a real, phone-native
terminal. A small Android app reshapes the session into a single-window **tabbed
terminal** built for touch, and a host package keeps that session **alive and
reachable** through reboots, network changes and process restarts — so your phone
can always find your shell again.

No port forwarding. No VPN. No account. No server to run. The host pushes its
current session address to a free, end-to-end-encrypted relay; the app reads it
back and reconnects automatically.

> **Heads up:** an sshx session URL grants full read/write access to your shell.
> Everything after `#` is the end-to-end decryption key and never reaches the
> sshx server. Treat it like a password. See [Security](#security).

---

## Features

- **Touch-native terminal** — one active terminal fills the screen; a top tab bar
switches/creates/closes sessions; a bottom bar sends real `Esc Tab Ctrl Alt - /`
and arrow keys. Keyboard-aware: the bars lift above the on-screen keyboard.
- **Swipe to scroll** the scrollback; start typing and you snap straight back to
the live prompt.
- **Lossless persistence** — your work lives in a dedicated tmux server, so a
dropped connection, a new session URL, or a reboot never loses a pane.
- **Auto-reconnect** — the app follows the host to its new address with no manual
re-pairing.
- **Self-healing sessions.** If the relay drops the session (long idle, a network
blip, waking from sleep), the host detects it and re-mints automatically, so you
never land on a stale blank screen.
- **Host windows sync to tabs** — open or close a window on the host and the app's
tabs follow, live.
- **Built-in chat & presence** — sshx's collaboration, reworked for mobile, with
an unread badge.
- **QR pairing** — scan once; the app remembers the resolver across session changes.
- **Tunable density** — pick your column count (default 80) for bigger or smaller text.

## How it works

```
your phone a free relay your machine
┌───────────────┐ reads addr ┌─────────────┐ pushes addr ┌──────────────────────┐
│ sshx-mobile │ ◀───────────── │ resolver │ ◀───────────── │ sshx ──▶ tmux (-L │
│ (the app) │ │ (MantleDB / │ │ sshx) │
│ │ ──────────────▶│ self-host) │ │ persistent, isolated │
└───────────────┘ sshx session └─────────────┘ └──────────────────────┘
│ ▲
└────────────── end-to-end encrypted sshx session ───────────────────┘
```

The host runs `sshx` against a persistent tmux server and publishes the current
(rotating) session URL to a **resolver** — by default a random, private
[MantleDB](https://mantledb.sh) namespace (no account, no email), or your own
loopback/LAN service. The app polls the resolver, connects to the session, and
re-syncs whenever the host's address changes.

---

## Install

You need two things: the **app** on your phone, and the **host tooling** on the
machine whose shell you want to reach.

> **Using Claude Code (or another coding agent)?** It can do the whole host setup for you,
> interactively — package, `sshx` CLI, always-on services, resolver + claim, verification, and
> wiring `claude` into the app's tmux. See
> [**Assisted setup with Claude Code**](./docs/setup-with-claude-code.md) (or run the bundled
> `/sshx-mobile-install` skill from a clone). You still install the APK and scan one QR.

### 1. The app (Android)

Download the latest `sshx-mobile-*.apk` from the
[**Releases**](https://github.com/moukrea/sshx-mobile/releases/latest) page and
install it (you may need to allow "install unknown apps" for your browser/file
manager).

You only sideload once: **the app updates itself.** On launch it checks the
Releases feed and, when a newer version exists, offers to download and install it
in-app (no Play Store, no re-sideloading) — see [Updating](#updating).

### 2. The host

`tmux`, `python3` and `qrencode` install automatically as package dependencies. The only extra
piece is the [`sshx`](https://sshx.io) CLI — it isn't in distro repos, so **`sshx-host-setup` offers
to install it for you** (or run `curl -sSf https://sshx.io/get | sh`, which puts it in `/usr/local/bin`).

Debian / Ubuntu (apt)

```bash
curl -fsSL https://moukrea.github.io/apt-repo/pubkey.gpg | sudo gpg --dearmor -o /usr/share/keyrings/moukrea.gpg
echo "deb [signed-by=/usr/share/keyrings/moukrea.gpg] https://moukrea.github.io/apt-repo stable main" | sudo tee /etc/apt/sources.list.d/moukrea.list
sudo apt update && sudo apt install sshx-mobile-host
```

Fedora / RHEL (dnf)

```bash
sudo tee /etc/yum.repos.d/moukrea.repo > /dev/null <<'EOF'
[moukrea]
name=moukrea
baseurl=https://moukrea.github.io/rpm-repo/
enabled=1
gpgcheck=1
gpgkey=https://moukrea.github.io/rpm-repo/pubkey.gpg
EOF
sudo dnf install sshx-mobile-host
```

macOS / Linux (Homebrew)

```bash
brew install moukrea/tap/sshx-mobile-host
```

Arch Linux

```bash
# Grab the PKGBUILD from the matching release and build it:
curl -fsSLO https://github.com/moukrea/sshx-mobile/releases/latest/download/PKGBUILD
makepkg -si
```

From source

```bash
git clone https://github.com/moukrea/sshx-mobile.git
cd sshx-mobile
host/install.sh
```

### 3. Connect

On the host, run the one-shot setup (as your normal user, **not** root):

```bash
sshx-host-setup
```

It generates a private resolver, enables the per-user services, and prints a **QR
code**. Open the app, tap **Scan QR**, point it at the code — done. Your shell is
now in your pocket, and it will stay reachable across reboots and network changes.

To check status later: `systemctl --user status sshx tmux-server sshx-tmux-bridge`.

### Updating

**The app updates itself.** On launch (from the home screen, so it never
interrupts a live session) it checks the GitHub Releases feed; if a newer version
is out it shows an **"Update available"** dialog. Tap **Update** and it downloads
that release's APK and hands it to the system installer — you grant "install
unknown apps" to the app once, the first time. **Later** re-prompts next launch;
**Skip** silences that one version. (An update can only self-install if it is
signed with the same key as the installed app, which holds for these releases; a
key change would need a one-time manual reinstall.)

**The host** you upgrade the way you installed it, then re-run setup so the new
version is the one actually running:

```bash
sudo apt update && sudo apt install --only-upgrade sshx-mobile-host # or: sudo dnf upgrade sshx-mobile-host / brew upgrade
sshx-host-setup
```

`sshx-host-setup` restarts `sshx` and the window bridge (lossless: your tmux
session is preserved, the phone re-resolves automatically) so the update takes
effect right away.

---

| Gesture / control | Action |
|---|---|
| Tap a tab | Switch terminal |
| Burger menu (top-right) | Columns (text size), new terminal, connected people, chat |
| `×` on a tab | Close that terminal |
| Bottom bar | `Esc Tab Ctrl Alt - /` and arrow keys (real terminal input) |
| Set your name (main menu) | Shown to others in the session, persisted across reconnects |

### Scrolling & selecting

Scrolling works on **both** surfaces, including inside full-screen TUIs such as
Claude Code (whose output lives in the scrollback):

- **Scroll** — on the **phone**, swipe up/down on the terminal; on a **laptop/desktop**
tmux client (`sshx-desk`, or a `claude` session wired through the app's tmux) use the
**mouse wheel**. To resume typing after scrolling up, scroll back to the bottom (or
press `q` / `Enter`).

**Selecting text on the desktop.** The wheel and plain-drag native selection can't both
be live at once — that's a tmux limitation, not a bug (tmux either owns the mouse for the
wheel, or releases it for selection). The desktop defaults to **wheel on**, so:

- Hold **Shift while you drag** to select — the terminal's own native selection (works on
every terminal, copies via the terminal itself). This is the standard way to select over
any mouse-aware program.
- Or run **`sshx-mouse off`** to switch that session to plain-drag native selection for a
while (the wheel then sends arrows until `sshx-mouse on`). `sshx-mouse` with no argument
toggles.

> The mouse setting is **per surface**: desktop sessions (`sshx-desk`, `claude` windows)
> keep `mouse on` for the wheel; phone tabs are `mouse off`, so a stray touch never drops
> the pane into tmux copy-mode. The phone keeps scrolling because its swipe is sent as
> keys, not mouse.

**Selecting text on the phone** is limited today: sshx renders the terminal with WebGL and
keeps its terminal object private, and sshx itself has no clipboard support
([sshx#77](https://github.com/ekzhang/sshx/issues/77)), so the app can't yet pull selected
text into the Android clipboard. Tracked for a follow-up (a tmux-capture bridge). For now,
copy from the desktop side.

Co-access from your desktop at the same time with `sshx-desk` (a normal tmux
client sharing the windows). Route `claude` (or any command) into the shared
session with the installed `claude-tmux.sh` helper so it shows up as a tab — the
host installer wires this into `~/.bashrc` automatically; for zsh add the
delegating wrapper from
[`docs/setup-with-claude-code.md`](./docs/setup-with-claude-code.md#wire-claude-into-the-apps-tmux).

---

## Troubleshooting

**App shows a blank/black screen with no terminals after scanning the QR.** The sshx
session the resolver points at has gone stale (sshx sessions expire after a long idle or
a network drop, and the host process doesn't always notice). **The host now detects this
and heals itself**: a liveness monitor re-mints the session automatically, usually within
a minute, and the app re-resolves and reconnects with **no re-scan needed**. Just wait a
moment. To force it immediately:

```bash
systemctl --user restart sshx.service
```

This self-heal is on by default; tune or disable it with `SSHX_HEALTH_*` (see
[`host/README.md`](./host/README.md#self-heal-a-stale-session-is-detected-and-re-minted-automatically)).
Periodic rotation is now optional (it only limits how long any one URL is exposed):

```bash
systemctl --user enable --now sshx-rotate.timer # default 1h; `systemctl --user edit sshx-rotate.timer` to change
```

**Check the host services are up:**

```bash
systemctl --user status tmux-server sshx sshx-tmux-bridge
```

**New host windows don't appear as tabs / a closed tab lingers.** The window list is
published by `sshx-tmux-bridge`; make sure it's `active`. More operational recipes
(reboot, no URL served, sizing) are in [`docs/runbook.md`](./docs/runbook.md).

---

## Security

- The session URL's `#fragment` is the **end-to-end encryption key**. sshx uses it
only in the browser; it never reaches the sshx server. Anyone with the full URL
can use your terminal.
- `sshx-qr` encodes the URL **offline** — it never sends it anywhere. Never paste
an sshx URL into an online QR generator.
- The resolver stores the full URL (incl. the key). The default MantleDB namespace
is random and private (the namespace itself is a bearer secret). **Claim it**
(`sshx-resolver-setup --claim`, no email) to attach a key: as of 2026-06 a claimed
namespace returns HTTP 401 without the key on **reads and writes** (verified — see
[`docs/setup-with-claude-code.md`](./docs/setup-with-claude-code.md)), so claiming is what makes a
*readable* namespace safe. For guaranteed read-privacy with zero third-party trust, run the bundled
self-hosted resolver over a network you control. Never expose a resolver to the public internet.
(Note: MantleDB lowercases namespaces on claim — use the lowercase form.)
- `host/resolver.env` (your live resolver config) is git-ignored and never
published. Only `resolver.env.example` ships.

---

## Build from source

**App:** `cd android && ./gradlew assembleRelease` → `app/build/outputs/apk/release/`.
Requires JDK 17 and the Android SDK. See [`android/BUILD.md`](./android/BUILD.md).

**Host packages:** `SSHX_VERSION=x.y.z nfpm pkg -p deb -f packaging/nfpm.yaml -t dist/`
(and `-p rpm`). CI builds and publishes them on every `x.y.z` tag — see
[`.github/workflows/`](./.github/workflows/).

## Design & internals

The lossless remote-access design, decisions and runbook live in
[`docs/`](./docs/): [setup-host](./docs/setup-host.md),
[setup-app](./docs/setup-app.md), [runbook](./docs/runbook.md), and the
[ADRs](./docs/adr/). Host tooling reference: [`host/README.md`](./host/README.md).
App internals: [`android/README.md`](./android/README.md).

## License

[MIT](./LICENSE) © moukrea