mobux
Your development machine in your pocket. mobux is a touch-native web UI for tmux: open any terminal session on your server straight from your phone, over your own private network.
The problem
Your real work lives on a server: builds, training runs, deploys, the long-lived shell with all your context in it. But you are not always at your desk. You are walking, commuting, away from the keyboard — and the thing you actually have on you is a phone.
A phone is a terrible terminal. Tiny keys, no control keys, no scrollback you can trust, browsers that fight you over the keyboard, and SSH apps that treat the screen like a 1980s VT100. So the work waits until you are back at a real machine.
mobux closes that gap. It puts your tmux sessions on the phone in a form built for a phone — touch gestures instead of key combos, a control-key ribbon where a keyboard would be, a reader view for scrollback, and a push notification when the long job finishes. The session keeps running on the server the whole time. The phone is just a good window into it.
What it does
-
Full terminal, touch-first. Attach to any tmux session and drive it by touch. A bottom input bar carries the keys a phone keyboard lacks —
^C, arrows,Tab,Esc, and more — on a scrollable ribbon. Two send modes: Enter executes; a separate inject button drops text into the line for readline editing without running it. -
Built for reading on a phone. A dedicated reader view renders scrollback with smooth, synthetic scrolling tuned for mobile WebViews, so long output is actually browsable. Pinch to zoom, swipe to switch windows.
-
Gestures, not chords. Swipe a session to rename or kill it. Swipe the terminal to move between tmux windows. Long-press for tmux commands. The things you'd reach for a key combo for become a gesture.
-
Notified when it matters. A long job finishing rings the terminal bell; mobux turns that into a Web Push notification on your phone — even with the screen locked — deep-linked back to the exact session. It hooks tmux's own bell event, so a notification means a real bell fired, not a guess scraped off the screen.
-
Voice capture. Record a voice note from the input bar and mobux transcribes it. Two providers: whisper running inside the mobux process (pure Rust, no container runtime), or any endpoint that speaks the OpenAI audio API — self-hosted whisper.cpp on your tailnet, or OpenAI's own. A ready-to-run, tailnet-only whisper.cpp recipe ships in
deploy/stt/.Three English checkpoints, picked in settings: base.en (the default) rides in the prebuilt release asset, so
install.shlands a host that dictates offline; tiny.en and small.en are their own release assets, downloaded on demand and checked against hashes compiled into the binary. A source build has to opt in, and pulls the same published assets on first use:# on aarch64, add: RUSTFLAGS="-C target-feature=+fp16"The default build has no engine and only talks to a configured endpoint. For an airgapped host, point
MOBUX_STT_MODEL_DIRat a directory holdingconfig.json,tokenizer.jsonandmodel.safetensors. Nothing contacts a model host at runtime.On arm64 the engine needs ARMv8.2 half-precision (FEAT_FP16) — a Raspberry Pi 4 and other ARMv8.0 cores do not have it, and mobux says so and refuses the local provider rather than crashing. Weights run at full precision, so a checkpoint costs about twice its download in memory: ~290 MB for base.en, ~150 MB for tiny.en, ~970 MB for small.en.
-
Reads output aloud. An optional listen mode speaks terminal output in a neural voice that runs on the host itself — no cloud, no account — falling back to the device's own voice where the host has none. What it reads is rewritten first: escape codes and box drawing are dropped, a long path reads as "main dot rs in src", a commit hash as "a hash", and a code block is announced ("bash, twelve lines") rather than recited, unless you ask for the whole thing.
-
Themed for night use. Muted, low-contrast color themes (Gruvbox Soft, Tomorrow Night Soft, Nord, Solarized, and more) chosen for a phone screen in a dark room, not a desktop in daylight.
-
Shell integration, one tap. Install OSC 133 prompt markers for bash, zsh, or fish from the settings page, so mobux can tell prompts from output and mark command boundaries cleanly under tmux.
-
A real app, not just a tab. Installable as a PWA, or build a signed Android app (Trusted Web Activity, package id
io.github.mvhenten.mobux) that mobux serves from/install— full-screen, a launcher icon, and OS-level push.
Security posture
mobux is meant to live on your private network, not the open internet.
- Private by network. The intended deployment is behind Tailscale: mobux is reachable only on your tailnet, never exposed publicly. The voice transcription recipe is the same — tailnet-only, no public surface.
- HTTPS on request. mobux serves plain HTTP by default, which is what you want behind a reverse proxy or a tunnel that already terminates TLS. Pass
--tls(or setMOBUX_TLS=1) and it generates and manages its own CA so phones can trust it — the/installpage walks you through adding the cert — or obtains a real Let's Encrypt certificate via ACME if you give it a public domain. Turn it on when nothing else terminates TLS: on a bare tailnet the credentials would otherwise cross the wire in clear text, and mobux says so loudly at startup. - PIN / Basic auth. Access is gated by HTTP Basic auth with a user and PIN you set in the config file, in an environment variable, or on the command line.
Cloudflare Tunnel and Access
To reach mobux without the tailnet, serve it through a Cloudflare Tunnel with
Cloudflare Access in front. Set the access block, run
mobux configure --cloudflared for the cloudflared config and the Access
settings, and follow Cloudflare Tunnel and Access.
The tailnet and the PIN keep working alongside.
Quick start
Install the prebuilt binary (Linux x86_64 or aarch64, needs tmux):
|
It picks the release asset matching uname -m, verifies its checksum, and installs to ~/.local/bin/mobux. On any other platform, cargo install mobux.
From source:
# Prerequisites: Rust, Node.js, tmux (run `make setup` to install the toolchain)
Then open https://<your-host>:5151 from a phone on the same tailnet. The
Makefile targets set MOBUX_TLS=1; a plain mobux run serves HTTP on the same
address unless you pass --tls.
Common targets:
Deploying to a phone (building the Android package, CA cert install, APK download, QR handoff) is self-service from the /install page — make twa is the same build for anyone who prefers a terminal. The full production runbook — systemd service, releasing, isolated dev instances — is in DEPLOY.md.
Every setting, with its config-file key, environment variable and flag, is in the config reference. mobux configure writes the file for you and mobux configure --schema prints its JSON schema. Running mobux behind a TLS-terminating path-prefix proxy takes three settings: behind a reverse proxy.
Architecture at a glance
┌──────────────┐ WebSocket ┌───────────────┐
│ Phone │◄──────────────────►│ mobux (Rust) │
│ terminal │ /ws/:session │ axum + PTY │
│ input bar │ │ tmux attach │
│ reader view │ REST API │ │
│ │◄──────────────────►│ /api/* │
└──────────────┘ └───────────────┘
- Backend — Rust. An axum server proxies a PTY-attached tmux session over a WebSocket and exposes a small REST API for session, pane, upload, and push management. The whole frontend is compiled into the binary, so the result is a single self-contained executable that runs from anywhere —
cargo install mobuxand go. - Frontend — JavaScript. A touch gesture recogniser, the mobile input bar, and the terminal view ship as bundled JS modules. The terminal renderer is pluggable: xterm.js is the stable default; an experimental clean-room renderer (sterk) can be switched on in settings as we modernize the rendering path.
- Notifications. Web Push (VAPID) driven by tmux's native
alert-bellhook.
Project status
mobux started as a personal tool and is being hardened into something others can run. It works today — it is the author's daily way to reach a dev box from a phone. It is open source under the MIT license. There is no hosted service, no accounts, and nothing phones home; you run it on your own machine.
The UI is being modernized toward a component-based single-page app. That migration is in progress; the current frontend described above is what ships today.
A short product overview for the curious is in OVERVIEW.md.
An agent on the host reads docs/for-agents.md for the MCP tools, the file server and the proxy.
API
| Endpoint | Method | Description |
|---|---|---|
/api/sessions |
GET / POST | List or create tmux sessions |
/api/sessions/:name/kill |
POST | Kill a session |
/api/sessions/:name/rename |
POST | Rename a session |
/api/sessions/:name/panes |
GET | List panes / windows |
/api/sessions/:name/command |
POST | Run a tmux command |
/api/sessions/:name/history |
GET | Capture scrollback |
/api/upload |
POST | Upload a file (multipart) |
/transcribe |
POST | Speech-to-text (OpenAI-compatible audio endpoint) |
/api/push/vapid-public-key |
GET | VAPID public key for push subscription |
/api/push/subscribe |
POST / DELETE | Register or unregister a push subscription |
/api/push/devices |
GET | List subscribed devices |
/api/push/notify |
POST | Send a push notification |
/ws/:name |
WS | Terminal WebSocket |
/api/install/apk/build |
POST | Start the Android package build (one at a time) |
/api/install/apk/status |
GET | Build phase, output tail, and whether an APK exists |
/install |
GET | Self-service install page (CA cert, APK, QR codes) — no auth |
/install/mobux.apk |
GET | Built APK download — no auth |
/install/mobux-ca.crt |
GET | Local CA cert for Android trust store — no auth |
/.well-known/assetlinks.json |
GET | Digital Asset Links file proving the APK owns the domain — no auth |
Dev telemetry
A dev-only client telemetry channel for debugging the frontend on a real
device. It is off by default and inert in production — gated behind the
MOBUX_DEV env var, read once at startup.
- Enable it: start mobux with
MOBUX_DEV=1(themobux-dev.serviceunit sets this). When set, the server logsdev mode: ON. - Where logs land: the frontend POSTs lines to
POST /api/telemetry(same-origin, behind normal auth, body capped at 64KB). The server writes each line to stderr / the journal prefixed[telemetry HH:MM:SS.mmm]. Tail withjournalctl --user -u mobux-dev -f(or/tmp/mobux*.logfor amakeinstance). WhenMOBUX_DEVis unset,/api/telemetryreturns 404 and nothing is logged. - From JS: the module is
web/static/telemetry.js, loaded once by the SPA's entrypoint. It resolves the dev flag itself from/api/build-info(no server-side HTML injection needed) and is a no-op until that resolves true. Import it anywhere for logging:
Each line carries a per-page session id so they're correlatable.import telemetry from '/static/telemetry.js'; telemetry.; // structured or a plain string - On-screen overlay: add
?telemetry=1to the URL, or runtelemetry.overlay(true)(alsowindow.mobuxTelemetry.overlay()in dev). The choice persists inlocalStorage['mobux:telemetry'].
License
MIT. Third-party attributions are in THIRD_PARTY-LICENSES.md.