OxiRemote

May 15, 2026 · View on GitHub

Self-hosted remote-anywhere agent + mobile-friendly web UI. Run a single binary on your dev machine, expose it through a Cloudflare Quick Tunnel, and reach your terminals, files, dev-server previews, and remote desktop from any browser.

Install

Until the first GitHub release ships, the install one-liner and npm install -g oxiremote will fail at the download step — they expect tagged release artifacts at https://github.com/nhtera/oxiremote/releases. Build from source (below) until then.

macOS / Linux

curl -fsSL https://raw.githubusercontent.com/nhtera/oxiremote/main/scripts/install.sh | bash

Drops oxiremote into $HOME/.local/bin. Override with OXIREMOTE_INSTALL_DIR=/usr/local/bin. The script verifies SHA256 against the release manifest before installing.

npm (any platform)

npm install -g oxiremote

The npm package is a thin wrapper — installing it downloads the matching prebuilt binary on postinstall. Same SHA256 verification.

Windows

Either install via npm (above) or download oxiremote-<version>-x86_64-pc-windows-msvc.zip from the Releases page, extract, and put oxiremote.exe on your PATH.

GitHub Codespaces

Drop the included .devcontainer/devcontainer.json into your repository (or copy it). On boot, Codespaces installs OxiRemote and starts it in headless mode (oxiremote --auto); the QR code prints to the Codespace console. Forwarded port 8787 exposes the dashboard locally; the Cloudflare tunnel URL is the address you scan from your phone.

From source

bun install
bun run build:release
./agent/target/release/oxiremote

Requires bun and a Rust toolchain.

Self-update

oxiremote update

Fetches the latest GitHub release for your target triple, verifies SHA256 against the published manifest, atomic-replaces the running binary. Restart the agent to pick up the new version. Set OXIREMOTE_VERSION=v0.2.3 (in scripts/install.sh) to pin a specific release; oxiremote update always tracks latest.

First run

On first launch the agent downloads cloudflared, opens a Quick Tunnel, and prints a pairing code in the TUI. Open the tunnel URL on your phone, scan the QR, enter the pairing code (or scan a deep-link QR with an active one-time key for one-tap pairing).

Environment variables

  • OXI_SECURE_COOKIES=1 — mark auth cookies as Secure (recommended over HTTPS / tunnel)
  • OXI_WORKSPACE=/path/to/project — set the workspace root (defaults to CWD)
  • OXI_HEADLESS=1 — force headless server mode even when a TTY is attached
  • OXI_VIDEO_PIPELINE=auto|av1|vp9|h264|jpeg — pick the remote-desktop transport. Default auto resolves in priority order AV1 → VP9 → H.264 → JPEG, AND-merged with the client's advertised codec list. Force a specific codec to fail-closed on incapable clients; force jpeg to disable WebRTC video entirely. AV1 + VP9 use libaom / libvpx with screen-content tuning (AOM_CONTENT_SCREEN + VP9E_CONTENT_SCREEN) to match Chrome Remote Desktop's bitrate-quality tradeoff.
  • OXIREMOTE_INSTALL_DIR=... — install script target directory
  • OXIREMOTE_BINARY_URL=... — npm wrapper download base URL (corp proxies / mirrors)

Remote desktop

Auto-negotiated WebRTC video at session start: AV1 → VP9 → H.264 → JPEG in priority order, picking the highest codec both server and client agree on. AV1 + VP9 use screen-content tuning (AOM_CONTENT_SCREEN, VP9E_CONTENT_SCREEN) and 16×16 dirty-rect active-map to match Chrome Remote Desktop's quality + speed. The chosen pipeline is shown in the desktop toolbar — hover the pill for the reason.

Browser support matrix (WebRTC receive):

CodecChromeSafariFirefox
AV1116+— (no plans 2026)130+
VP9alliPad ≥ 17all
H.264allallall

Operators who need to opt out of WebRTC: OXI_VIDEO_PIPELINE=jpeg. Headless builds without video: cargo build --no-default-features --features desktop. Build-time system deps: libvpx (VP9) and libaom ≥ 3.0 (AV1) — brew install libvpx aom on macOS, apt install libvpx-dev libaom-dev on Linux, vcpkg libvpx aom on Windows.

Mobile usage

Tap the Type to send to remote bar pinned above the on-screen keys to bring up the iOS/Android keyboard. Hit Send (or Enter) to dispatch the entire string at once — useful for passwords, command snippets, and pasted text. Punctuation, accented characters, and emoji are all supported. The Sheet button next to the input opens a multiline composer for long pastes.

Note (macOS): the lock screen accepts text from this composer for screensaver / session-lock states. FileVault pre-login unlock is not yet supported — Apple's EnableSecureEventInput blocks synthetic keystrokes at the loginwindow. Workaround: use Touch ID, Apple Watch unlock, or the host's physical keyboard for the initial post-boot login.

Notifications (Web Push)

The agent runs a Web Push server. Install the web UI as a PWA on your phone (Add to Home Screen on iOS), enable notifications from the in-app banner, then trigger a push from the shell:

oxiremote notify --title "build done" \
  --body "vite production build OK" \
  --deep-link "/h/<host_id>/terminal/<session_id>"

Tapping the notification opens the deep link on the correct host. The CLI reads ~/.oxiremote/notify.token (chmod 0600, created on first run) to authenticate against the localhost /api/notify endpoint.

Named tunnels (production)

Quick Tunnels are great for trying things out, but they hardcode ha-connections=1: every HTTP and WebSocket request from your client funnels through a single QUIC connection to a single Cloudflare edge POP. When that connection has a transient stream-listener hiccup — which Cloudflare itself flags as expected for account-less tunnels ("no uptime guarantee") — in-flight WS upgrades fail at the edge and never reach the agent. The SPA reconnects automatically, but you'll occasionally see "Connection lost" flashes.

The agent ships several mitigations that make Quick Tunnels pretty usable in practice — the worker reverse-proxy hides DNS-propagation lag after sleep/wake, an edge-health monitor auto-respawns a stuck cloudflared, and the dashboard surfaces a Tunnel unhealthy chip with the reason instead of lying. If you want a permanent hostname that never rotates, run a Named Tunnel:

A Named Tunnel runs with ha-connections=4 across multiple edge POPs and gets you a stable hostname that doesn't rotate per process. Setup is one-time:

# 1. Write the config scaffold
oxiremote tunnel use my-tunnel-name

# 2. Create the tunnel and route DNS (standard cloudflared commands)
cloudflared tunnel login
cloudflared tunnel create my-tunnel-name
cloudflared tunnel route dns my-tunnel-name oxi.example.com

When ~/.config/oxiremote/tunnel.toml is present, the agent skips Quick Tunnel and runs your named tunnel. Edit the file to point at a specific credentials_file if cloudflared can't auto-discover it.

Dev

Run both agent + web UI:

bun dev

In dev mode, the agent serves API endpoints and the Vite dev server handles the React UI at localhost:5173.

Run only one side:

bun run dev:web    # web UI only
bun run dev:agent  # agent only

Tests

bun run test:web                                          # vitest unit tests (pure logic)
cargo test --manifest-path agent/Cargo.toml               # rust unit + integration tests
OXI_PAIRING_CODE=<code> bun run e2e                       # playwright (needs running agent)

E2E opt-in env vars (specs test.skip themselves when unset):

VarPurpose
OXI_PAIRING_CODERequired for any spec that pairs a device. Fresh 8-char pairing code; consumed once by global-setup.ts.
OXI_E2E_OTK=1Enables otk-approval-desktop.spec.ts (localhost-only OTK + approval flow).
OXI_E2E_DESKTOP=1Enables desktop-permission-revoked.spec.ts (capture-end signal verification — planned).
OXI_E2E_VISUAL=1Enables visual-snapshot specs (planned, gated to avoid CI flake).

Releases (maintainers)

git tag v0.1.0
git push --tags

The release GitHub Action builds the binary on each target's native runner, generates a single SHA256 manifest, and uploads everything to the matching GitHub Release. Re-running with workflow_dispatch rebuilds without re-tagging.

Structure

  • agent/ — Rust local agent (HTTP server, tunnel, services, tray, TUI)
  • apps/web/ — React/TS web UI (Vite)
  • npm-wrapper/ — thin npm package that downloads the prebuilt binary
  • scripts/install.sh — curl-pipe-sh installer
  • .devcontainer/ — Codespaces config
  • .github/workflows/release.yml — multi-platform release builder