Skip to content

connorads/remobi

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

252 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

remobi logo

remobi

CI npm licence

Your terminal. Everywhere.

Your tmux session, on your phone. Same panes, same windows, same bindings β€” nothing changes on your computer. Swipe between windows, pinch to zoom, tap to send commands. You just get a remote control.

It's a terminal on a 6-inch screen. It won't win design awards. But you can do everything β€” monitor coding agents, intervene when they're stuck, scroll through output, switch contexts. Full power.

/bin/bash -c "$(curl -fsSL http://remobi.app/install.sh)"

To upgrade stable: npm install -g remobi@latest

To try the experimental channel: npm install -g remobi@dev

Your coding agent handles the rest. It installs remobi, inspects your tmux config, generates a config, and suggests tweaks to make your tmux more mobile-friendly β€” one conversation. Works with Claude Code and Codex.

Why remobi

  • Zero workflow changes β€” your existing tmux setup, untouched
  • Swipe between windows β€” gesture navigation, no prefix key fumbling on a phone screen
  • Pinch to zoom β€” resize text like every other app on your phone
  • Install to your home screen β€” standalone PWA, looks and feels native
  • Config-driven β€” your buttons, your gestures, your layout
  • Self-hosted β€” local-first by default. Bring your own access layer (Tailscale, Cloudflare, ngrok)
remobi.mp4

Requirements

Manual setup

# 1. Install
npm install -g remobi

# 2. Enable mouse mode in tmux (required for touch scroll and tap-to-focus)
#    Add to ~/.config/tmux/tmux.conf or ~/.tmux.conf:
#    set -g mouse on

# 3. Start (spawns your command, serves remobi on 127.0.0.1:7681)
remobi serve

Essential tmux settings for mobile β€” add to your tmux.conf if not already present:

Setting Command Why
Mouse mode set -g mouse on Enables touch scroll, tap-to-focus, drag resize. The single highest-value setting for mobile use.
Status position set -g status-position top Keeps status bar away from remobi's touch toolbar at the bottom
Renumber windows set -g renumber-windows on Keeps window list tidy after closing windows

See Mobile-friendly tmux config for responsive status bars, popup sizing, and more.

For local development, see the Development section below.

Open http://localhost:7681 on the same machine to verify it works. For phone access, put a trusted proxy/tunnel in front of it, for example Tailscale Serve. If your proxy mounts remobi under a URL prefix, start remobi with --base-path /that-prefix so the HTML, PWA links, and WebSocket all use the same external path.

Using with zellij

zellij is a batteries-included tmux alternative with a discoverable, modal UI. remobi serves it the same way it serves tmux:

remobi serve -- zellij attach --create main

zellij attach --create <name> attaches or creates, like tmux new-session -A. Sessions persist across remobi restarts (zellij detaches rather than dying when the client goes away).

zellij needs no extra setup for mobile:

  • Mouse mode is on by default, so touch scroll and tap-to-focus just work β€” no set -g mouse on equivalent to remember
  • Stock zellij ships a tmux-compat mode on Ctrl-B, so remobi's Prefix button and the default swipe gestures (prefix+n/prefix+p β€” next/previous tab) work unchanged, as do the New Window (prefix+c), Split (prefix+%/prefix+"), Zoom (prefix+z), Copy/scrollback (prefix+[), and Kill (prefix+x) drawer buttons
  • Its native modal shortcuts (Ctrl+t for tabs, Ctrl+p for panes, …) also pass through fine

Only three drawer buttons send sequences zellij doesn't bind. A remobi.config.ts with zellij equivalents:

export default {
  name: 'zellij',
  drawer: {
    buttons: (defaults) => [
      ...defaults.filter((b) => !['tmux-sessions', 'tmux-windows', 'tmux-help'].includes(b.id)),
      { id: 'zellij-sessions', label: 'Sessions', description: 'Open session manager', action: { type: 'send', data: '\x0fw' } },
      { id: 'zellij-lock', label: 'Lock', description: 'Toggle locked mode (pass Ctrl keys through)', action: { type: 'send', data: '\x07' } },
    ],
  },
}

zellij-lock toggles zellij's locked mode, which passes Ctrl keys straight through β€” handy when a TUI inside the session wants shortcuts zellij would otherwise capture. If you also stay attached from your desktop while remobi is connected, set mirror_session true in ~/.config/zellij/config.kdl so both clients see the same view. For interactive onboarding, point an AI agent at the remobi-setup skill, which covers the zellij path.

Using with herdr

herdr is an agent multiplexer β€” a tmux alternative purpose-built for supervising AI coding agents, with per-pane agent status detection. remobi serves it the same way it serves tmux:

remobi serve -- herdr --session main

herdr --session <name> attaches or creates, like tmux new-session -A. Sessions persist across remobi restarts.

herdr needs no extra setup for mobile:

  • Mouse capture is on by default, so touch scroll and tap-to-focus just work β€” no set -g mouse on equivalent to remember
  • Its default prefix is Ctrl-B, the same as tmux, so remobi's Prefix button and the default swipe gestures (prefix+n/prefix+p β€” next/previous tab) work unchanged, as do the New Window (prefix+c), Zoom (prefix+z), Kill (prefix+x), and Help (prefix+?) drawer buttons
  • It has a built-in single-column mobile layout for narrow terminals (ui.mobile_width_threshold in herdr's config)

Only a few drawer buttons send sequences herdr doesn't bind. A remobi.config.ts with herdr equivalents:

export default {
  name: 'herdr',
  drawer: {
    buttons: (defaults) => [
      ...defaults.filter(
        (b) => !['tmux-split-vertical', 'tmux-split-horizontal', 'tmux-sessions', 'tmux-windows', 'tmux-copy'].includes(b.id),
      ),
      { id: 'herdr-split-v', label: 'Split |', description: 'Split pane side-by-side', action: { type: 'send', data: '\x02v' } },
      { id: 'herdr-split-h', label: 'Split β€”', description: 'Split pane stacked', action: { type: 'send', data: '\x02-' } },
      { id: 'herdr-workspaces', label: 'Spaces', description: 'Workspace picker', action: { type: 'send', data: '\x02w' } },
      { id: 'herdr-sidebar', label: 'Sidebar', description: 'Toggle agent sidebar', action: { type: 'send', data: '\x02b' } },
      { id: 'herdr-scrollback', label: 'Scroll', description: 'Edit scrollback', action: { type: 'send', data: '\x02e' } },
    ],
  },
}

tmux-windows is filtered out because in herdr prefix+w opens the workspace picker β€” herdr-workspaces re-adds the same sequence with an accurate label. For interactive onboarding, point an AI agent at the remobi-setup skill, which covers the herdr path.

Release channels

  • main publishes stable releases to npm latest
  • dev publishes prereleases to npm dev
  • merge dev into main to promote an experimental line to stable

If an experimental change is breaking for consumers, include a BREAKING CHANGE: footer so semantic-release computes the right next version on both channels. ! in the header is optional shorthand only; on its own it does not trigger a major release in this repo.

Set up with AI

The setup skill checks your environment, inspects your tmux config, generates a remobi.config.ts, and suggests tmux mobile optimisations β€” one conversation. Three ways to use it, from simplest to most manual:

Option 1: One-liner β€” installs the skill and launches an interactive session with your coding agent:

/bin/bash -c "$(curl -fsSL http://remobi.app/install.sh)"

Option 2: Install the skill β€” if you prefer to start the conversation yourself:

npx skills add connorads/remobi

Then tell your coding agent: "Use the remobi-setup skill to onboard me."

Option 3: Already in a session? β€” if an agent is already looking at this repo (or you don't want to install a skill), tell it:

Read .agents/skills/remobi-setup/SKILL.md in this repo and follow it to onboard me.

The skill file contains the full setup workflow. The agent can read it directly β€” no installation needed.

Security model

remobi is a remote-control surface for your terminal. Anyone who can reach it can drive the tmux session with your user privileges.

  • remobi serve binds to 127.0.0.1 by default.
  • The inner PTY-backed terminal session stays local to the remobi process.
  • There is no built-in login, password, or ACL in remobi itself.
  • Safe default: keep it on localhost and publish it through a trusted layer like Tailscale Serve.
  • If you use remobi serve --host 0.0.0.0, you are exposing terminal control to your LAN/whatever can route to that port. Do that only if you intentionally want direct network exposure and have separate network controls in place.

To report a vulnerability, see SECURITY.md.

CLI reference

remobi serve [--config <path>] [--port <n>] [--host <addr>] [--base-path <path>] [-- <command...>]
  Start remobi with its built-in web terminal and PWA support.
  Default host: 127.0.0.1. Default port: 7681. Default command: tmux new-session -A -s main
  Example: remobi serve --host 0.0.0.0 --port 8080
  Example: remobi serve --base-path /random-token
  Example: remobi serve --port 8080 -- tmux new -As dev

remobi build [--config <path>] [--output <path>] [--dry-run]
  Deprecated. remobi no longer patches ttyd HTML.

remobi inject [--config <path>] [--dry-run]
  Deprecated. remobi no longer patches ttyd HTML.

remobi init
  Scaffold a remobi.config.ts with commented defaults.

remobi --version
remobi --help

Short flags: -c (--config), -p (--port). Legacy deprecated flags: -o (--output), -n (--dry-run).

Config resolution

When --config is not specified, remobi searches:

  1. remobi.config.ts / .js in the current directory
  2. ~/.config/remobi/remobi.config.ts / .js (XDG fallback)

Configuration

Create remobi.config.ts (or run remobi init):

export default {
  font: {
    family: 'JetBrainsMono NFM, monospace',
    mobileSizeDefault: 16,
    sizeRange: [8, 32],
  },
  toolbar: {
    row1: [
      { id: 'esc', label: 'Esc', description: 'Send Escape key', action: { type: 'send', data: '\x1b' } },
      { id: 'tmux-prefix', label: 'Prefix', description: 'Send tmux prefix key (Ctrl-B)', action: { type: 'send', data: '\x02' } },
      // ...
    ],
    row2: [
      { id: 'alt-enter', label: 'M-↡', description: 'Send Alt+Enter (ESC + Enter)', action: { type: 'send', data: '\x1b\r' } },
      { id: 'drawer-toggle', label: '☰ More', description: 'Open command drawer', action: { type: 'drawer-toggle' } },
      { id: 'paste', label: 'Paste', description: 'Paste from clipboard', action: { type: 'paste' } },
      { id: 'backspace', label: '⌫', description: 'Send Backspace key', action: { type: 'send', data: '\x7f' } },
      // ...
    ],
  },
  drawer: {
    buttons: [
      { id: 'tmux-new-window', label: '+ Win', description: 'Create tmux window', action: { type: 'send', data: '\x02c' } },
      { id: 'tmux-split-vertical', label: 'Split |', description: 'Split pane vertically', action: { type: 'send', data: '\x02%' } },
      { id: 'combo-picker', label: 'Combo', description: 'Open combo sender (Ctrl/Alt + key)', action: { type: 'combo-picker' } },
      // ...
    ],
  },
  gestures: {
    swipe: {
      enabled: true,
      left: '\x02n',         // data sent on swipe left (default: next tmux window)
      right: '\x02p',        // data sent on swipe right (default: prev tmux window)
      leftLabel: 'Next tmux window',    // shown in help overlay
      rightLabel: 'Previous tmux window',
    },
    scroll: {
      enabled: true,
      strategy: 'wheel',
      sensitivity: 40,
      wheelIntervalMs: 24,
    },
    pinch: { enabled: true },
  },
  mobile: {
    initData: '\x02z',     // send on mobile load when viewport < widthThreshold
    widthThreshold: 768,   // px β€” default matches common phone/tablet breakpoint
  },
  floatingButtons: [
    {
      position: 'top-left',
      buttons: [
        { id: 'zoom', label: 'Zoom', description: 'Toggle pane zoom', action: { type: 'send', data: '\x02z' } },
      ],
    },
  ],
}

All fields are optional β€” the CLI fills in defaults internally when it loads the config.

Shipped tmux drawer defaults stick to stock tmux bindings (c, %, ", s, w, [, ?, x, z) rather than personal popup workflows.

Replace the drawer entirely with a plain array when you want a fully custom setup:

import { defineConfig } from 'remobi/config'

export default defineConfig({
  drawer: {
    buttons: [
      { id: 'sessions', label: 'Sessions', description: 'Choose tmux session', action: { type: 'send', data: '\x02s' } },
      { id: 'git', label: 'Git', description: 'Open my tmux git popup', action: { type: 'send', data: '\x02g' } },
    ],
  },
})

At runtime, remobi validates the config object shape and rejects unknown keys with clear path-based errors.

gestures.scroll.strategy controls touch scroll behaviour:

  • wheel (default): sends SGR mouse wheel events with touch-mapped terminal coordinates.
  • keys: sends PageUp / PageDown for app-level paging when preferred.

Programmatic API

import { defineConfig, serialiseThemeForTtyd } from 'remobi/config'
import type { RemobiConfig, ControlButton } from 'remobi/types'
import { init } from 'remobi'

Advanced consumers can use hook registry primitives to observe lifecycle and terminal-send events:

import { createHookRegistry, init } from 'remobi'

const hooks = createHookRegistry()
hooks.on('beforeSendData', (ctx) => {
  if (ctx.data.includes('rm -rf /')) return { block: true }
})

init(undefined, hooks)

Guides

Architecture docs

Architecture

Pure TypeScript + DOM API β€” no framework. The build bundles the browser client via esbuild, serves it from Node, and bridges browser input/output to a local PTY via node-pty. xterm.js handles terminal rendering in the browser; remobi layers the mobile controls on top. The docs above walk through the current runtime in more detail, including diagrams for the server, browser, and WebSocket flow.

Key modules:

Module Purpose
src/toolbar/ Two-row touch toolbar
src/drawer/ Command drawer with grid layout
src/gestures/ Swipe, pinch, scroll detection
src/controls/ Font size, help overlay, scroll buttons
src/theme/ Catppuccin Mocha + theme application
src/viewport/ Height management, landscape detection
src/util/ DOM helpers, terminal, keyboard, haptics

Public API and semver

remobi follows semantic versioning. The public API is defined by the following import paths:

Import path Contents Stability
remobi init, defineConfig, createHookRegistry, RemobiConfig, ControlButton, ButtonAction, ButtonArrayPatch, ButtonArrayInput, RemobiConfigOverrides, HookRegistry Public β€” breaking changes are semver-major
remobi/config defineConfig, mergeConfig, defaultConfig, serialiseThemeForTtyd Public
remobi/types All types in src/types.ts Public

Internal modules (not part of the public API β€” may change without a major version bump): src/toolbar/, src/drawer/, src/gestures/, src/controls/, src/theme/, src/viewport/, src/util/, src/serve.ts, src/cli/, build.ts

Semver policy:

  • Major: removing or renaming a public export, changing a public function signature incompatibly, removing a config field
  • Minor: adding new public exports, new optional config fields, new ButtonArrayInput operations
  • Patch: bug fixes, internal refactors, documentation updates

Development

git clone https://github.com/connorads/remobi.git && cd remobi
pnpm install
git config core.hooksPath .hk-hooks   # enable commit hooks (conventional commits, biome)

Running locally

From source (bundles the browser client on the fly via esbuild β€” no build step needed):

tsx cli.ts serve              # localhost:7681, default tmux session

Or build first, then run from dist/:

pnpm run build:dist          # transpile TS β†’ JS + bundle browser client
node dist/cli.mjs serve      # run locally-built version on localhost:7681

No watch mode β€” re-run the build or use tsx for automatic source bundling.

Checks

pnpm test            # vitest (unit + integration)
pnpm run test:pw     # playwright e2e (needs: pnpm exec playwright install chromium webkit --with-deps)
pnpm run check       # biome lint + format

FAQ

Is this secure? remobi doesn't handle auth β€” it's a UI overlay. Use a tunnel or VPN you trust. We recommend Tailscale (deployment guide included) β€” your session never leaves your tailnet. Cloudflare Tunnel and ngrok also work. Security is your responsibility.

Why not Termux / Termius / SSH apps? They work. But you're managing SSH keys, losing your tmux setup, and fighting a UI that wasn't built for touch. remobi keeps your exact workflow β€” same panes, same windows, same bindings β€” and adds touch controls on top.

Why not Happy / Claude resume / chat-based mobile apps? Those tools change your workflow. Chat relays route through third-party servers. Claude's resume has limitations. remobi gives you the raw terminal β€” full power, self-hosted, works with every agent because it works with tmux.

Why Node? remobi migrated from Bun to Node.js + pnpm for broader compatibility. It transpiles to JS via tsdown for npm distribution and uses esbuild for the browser client bundle.

Is this production-ready? It's v0.1. The author uses it daily. It works. It's also early β€” feedback welcome, forks encouraged.

Acknowledgements

remobi owns the full web terminal path now: a local PTY on the server, xterm.js in the browser, and the mobile touch controls on top.

Earlier versions were built on top of ttyd. It helped remobi launch quickly, prove the workflow, and shape the product before the runtime moved in-house.

Licence

MIT