remobi-setup
remobi-setup
Interactive onboarding skill for remobi — monitor and control tmux from your phone.
This skill walks the user through setup in one conversation. The guiding principle: detect everything possible, default everything sensible, ask only what requires human intent. Most users answer 1-3 questions total.
Workflow
Phase 1: Welcome and understand (1 question)
Open with a one-liner confirming what they're getting, then ask what brings them here:
"remobi puts your tmux session on your phone — same panes, same windows, touch controls on top. Everything we set up here you can change later."
"What brings you to remobi? For example: monitoring coding agents from your phone, getting phone access to your dev sessions, or just curious to try it out."
Map the answer to a persona internally (don't tell the user their "persona"):
| Persona | Signals | Downstream effect |
|---|---|---|
| Agent Watcher | Mentions coding agents, Claude Code, Codex, AI, monitoring | Auto-zoom on, floating zoom button, double-tap zoom enabled, lean config, minimal questions |
| Remote Dev | Mentions tmux, SSH, dev workflow, existing setup | Inspect config thoroughly, offer popup drawer buttons, ask about auto-zoom |
| Newcomer | Says curious, trying it out, heard about it, no specific use case | Offer tmux setup, explain concepts, auto-zoom on, sensible defaults |
If the answer is ambiguous, lean towards Agent Watcher — it's the most common path and the defaults work well for everyone.
Phase 2: Environment and tmux setup
Check prerequisites
Run silently, then report what's present vs missing:
node --version # need >= 22
tmux -V # target multiplexer
which remobi # npm install -g remobi
If anything is missing, help install it:
- Node: suggest mise, nvm, or direct install
- tmux:
brew install tmuxor distro package - remobi:
npm install -g remobi
Inspect tmux
Gather the user's tmux configuration to inform config generation.
tmux show-options -g prefix # prefix key
tmux list-keys # all bindings
tmux show-options -g mouse # mouse mode
tmux show-options -g status-left # status bar
tmux show-options -g status-position # top or bottom
tmux list-keys | grep display-popup # popup bindings
If tmux isn't running, fall back to reading the config file directly:
cat ~/.config/tmux/tmux.conf 2>/dev/null || cat ~/.tmux.conf 2>/dev/null
Auto-detect and note:
- Prefix key and byte (Ctrl-B =
\x02, Ctrl-A =\x01, etc.) - Custom popup bindings (lazygit, yazi, scratch shell, system monitor, etc.)
- Whether mouse mode is on
- Split bindings (stock
%/"or remapped|/-) - Status bar complexity and position
- Plugin manager (tpm, etc.)
- Double-tap zoom gesture (see
references/mobile-panes.mdfor pane workflows)
Detect installed tools — check for popular tools that work well as tmux popup bindings:
which lazygit # Git TUI
which yazi # File manager
which btm || which htop # System monitor
which nvim || which vim # Editor
Offer tmux setup (Agent Watcher and Newcomer only)
If no tmux config exists, read references/tmux-basics.md and offer to create one. Frame it as a proposal, not a gap:
Agent Watcher framing:
"I'll create a tmux config tuned for monitoring agents — mouse support, status bar at top, and double-tap zoom so you can zoom into any agent pane on your phone. Go ahead?"
Newcomer framing:
"tmux is the terminal multiplexer that remobi sits on top of — it keeps your sessions running even when you disconnect. I'll set up a config with mouse support, sensible defaults, and a help popup to learn the keybindings. Want me to explain what each setting does as I go?"
Remote Dev: Skip — they already have a config.
The starter config comes from references/tmux-basics.md. For Agent Watchers, include the "Agent watcher starter config" section (zoom indicator, auto-rename, double-tap zoom via remobi config).
For Newcomers with detected tools, also offer popup bindings:
"I found lazygit and yazi on your system. These work great as tmux popups — one keypress to open a floating window. Want me to add popup bindings for them?"
Only proceed to Phase 3 once the user has a working tmux session.
Phase 3: Confirm detections and ask what's needed (0-3 questions)
Present a summary of what you found and what you plan to configure. The style is "here's what I'll do" with checkpoints, not an interview.
Summary format:
"Based on your setup, here's what I'll configure:
- Prefix: Ctrl-B (detected from your tmux config)
- Auto-zoom on mobile load (pane fills the phone screen)
- Floating zoom button (one-tap zoom toggle)
- Default toolbar and drawer buttons
- [If applicable:] Drawer buttons for lazygit and yazi (matching your popup bindings)"
Then ask only questions that can't be detected or defaulted:
Questions by persona
Agent Watcher (0-1 questions):
If popup bindings or tools were detected:
"I found [lazygit/yazi/btm] on your system and matching popup bindings. Want drawer buttons for these in remobi so you can trigger them from your phone?"
If nothing special detected: zero questions — proceed straight to config generation.
Remote Dev (1-3 questions):
Question 1 (if popup bindings or tools detected):
"I found popup bindings for [list]. Want matching drawer buttons in remobi?"
Question 2 (if multi-pane layout likely):
"Do you want auto-zoom when you open remobi on your phone? This zooms the current pane to full screen — works well with multi-pane layouts on a small screen."
Question 3 (catch-all):
"Anything else you want accessible from your phone? Custom tmux bindings, specific tools, anything I missed?"
Newcomer (0-1 questions):
If tools were detected and popup bindings were set up in Phase 2:
"I set up popup bindings for [lazygit/yazi]. Want matching buttons in remobi's command drawer?"
Otherwise: zero questions — defaults are great to start with.
Summarise what you've gathered before moving to config generation.
Phase 4: Generate config and suggest tmux tweaks
Generate remobi.config.ts
Export a plain config object — only include keys that differ from defaults, omit everything else. Do not import { defineConfig } from 'remobi' — the CLI calls defineConfig() internally so the config just needs a plain object export.
export default {
// Only non-default overrides here
}
Place at ~/.config/remobi/remobi.config.ts (XDG location) unless the user prefers elsewhere.
After writing, validate by starting remobi. remobi auto-discovers config from the current directory first, then ~/.config/remobi/, so --config is only needed when you want to force a specific file:
remobi serve --port 18765 -- /bin/true
A zero exit means the config loaded and the command started cleanly. If the user stored config somewhere custom, validate that path explicitly instead:
remobi serve --config /path/to/remobi.config.ts --port 18765 -- /bin/true
Fix any errors and re-validate until clean.
See Config reference below for the full schema, allowed keys, action types, and escape codes.
Suggest tmux mobile optimisations (Remote Dev only)
For Remote Dev users who already had a tmux config, offer mobile tweaks as a single confirmation. Read references/mobile-tmux.md and references/mobile-panes.md for full context.
"I have a few suggestions to make your tmux more mobile-friendly: [list 2-3 most impactful items]. Want me to add these to your tmux.conf?"
Prioritise by impact, suggest maximum 3:
- Double-tap zoom (if multi-pane user — enable via remobi
gestures.doubleTap) - Responsive status bar (if status bar would overflow on phone — see
references/mobile-tmux.md) - Zoom indicator (if
#{window_zoomed_flag}missing from status)
Also check and mention (but don't push):
| Check | Command | Good sign | Suggestion if missing |
|---|---|---|---|
| Mouse mode | tmux show -g mouse |
on |
set -g mouse on |
| Status position | tmux show -g status-position |
top |
set -g status-position top (keeps status away from remobi toolbar) |
| Popup sizing | tmux list-keys | grep display-popup |
Uses % dimensions |
Replace fixed char sizes with 95%/100% |
| Window renumbering | tmux show -g renumber-windows |
on |
set -g renumber-windows on |
Suggest snippets only — never modify tmux.conf without explicit permission.
Skip this for Newcomers — their starter config from Phase 2 already includes the essentials.
Phase 5: Deploy and wrap up
Deployment
Detect what's available and recommend accordingly:
which tailscale # check for Tailscale
If Tailscale installed: recommend Tailscale Serve directly:
"I see Tailscale on your system. Tailscale Serve is the simplest way to access remobi from your phone — HTTPS over your private network, no extra setup."
Read references/tailscale-serve.md for the full guide.
If no Tailscale: offer options:
"To access remobi from your phone, you need to put it behind a trusted network layer. Options:
- Tailscale Serve (recommended) — private VPN, HTTPS, easiest setup
- Cloudflare Tunnel + Access — private tunnel with access policies
- Local network — if your phone is on the same WiFi/VPN"
remobi is a remote-control surface for your terminal — never expose it to the public internet. All deployment options keep access private.
Security hardening
remobi hardens the connection even on private networks. Mention these if the user has security concerns:
- Binds
127.0.0.1only — never exposed to network without explicit--hostflag - Content-Security-Policy — strict default-src, script-src, connect-src scoped to same host
- WebSocket origin validation — rejects cross-origin upgrade requests
- Relay buffer limit — 1 MB per connection; drops oversized payloads
- Local-only default — remobi binds to
127.0.0.1unless the user explicitly changes--host - X-Frame-Options DENY — prevents clickjacking via iframes
- Referrer-Policy: no-referrer — no URL leaking to external sites
For macOS users, mention --no-sleep and point to references/keep-awake.md for persistent options.
For users migrating from old ttyd-based setups, point to references/ttyd-flags.md as legacy guidance only.
Summary
Tell the user:
- What was configured and why (prefix byte, custom bindings, gestures, auto-zoom)
- How to start:
remobi serve - How to access from their phone (URL from deployment choice)
- PWA install: on mobile, tap "Add to Home Screen" for a standalone app experience
- Built-in mobile controls (these work out of the box, no config needed):
- Font size:
+/-buttons in top-right. Config:font.mobileSizeDefault(default 16px),font.sizeRange(default [8, 32]), steps by 2 - Scroll buttons: Floating arrow buttons on the sides. Long-press for rapid repeat (300ms delay, 100ms interval). Auto-fade after 2s. Strategy follows
gestures.scroll.strategy(wheelsends mouse events,keyssends PageUp/PageDown) - Combo picker: Modal for arbitrary key combos — type
C-s,M-Enter,Alt-x,C-[. Supports Ctrl, Alt, Shift modifiers + named keys (PageUp, Escape, etc.). Opened via drawer "Combo" button - Help overlay:
?button in top-right. Shows all configured buttons, gestures, and floating buttons in tables. Config-driven, updates when you change buttons - Landscape + keyboard: When on-screen keyboard opens in landscape, row 2 auto-hides and buttons shrink. No config needed
- Font size:
- PWA: enabled by default. On mobile Safari/Chrome, tap Share then "Add to Home Screen" for standalone app experience. Config options:
pwa.enabled(defaulttrue) — setfalseto disable manifest + iconspwa.themeColor(default'#1e1e2e') — status bar colour on mobilepwa.shortName(optional) — short name for home screen icon (falls back toname)
- This is a starting point — not a locked-in config. Run this skill again any time to tweak buttons, add drawer commands, or change gestures.
Config reference
Allowed root keys
Exactly these — validation rejects anything else:
name theme font toolbar drawer gestures mobile floatingButtons pwa reconnect
ButtonAction union
type |
Required fields | Notes |
|---|---|---|
send |
data: string |
Optional keyLabel?: string for help overlay |
prefix |
data: string |
Sends prefix byte then opens combo picker for follow-up key. Use { type: 'send', data: '\x02' } for raw prefix-only behaviour |
ctrl-modifier |
(none) | Opens Ctrl+key combo UI |
paste |
(none) | Paste from clipboard |
combo-picker |
(none) | Opens Ctrl/Alt + key modal |
drawer-toggle |
(none) | Opens/closes command drawer |
Non-send/prefix actions must NOT have data or keyLabel — the validator rejects them.
ControlButton shape
Every button in toolbar rows, drawer, and floatingButtons uses this schema:
{
id: string // unique within its array
label: string // text shown on the button
description: string // shown in help overlay — keep user-facing and clear
action: ButtonAction
}
Button array forms (toolbar.row1, toolbar.row2, drawer.buttons)
Two forms — pick the least invasive:
// 1. Replace entirely (plain array)
toolbar: { row1: [{ id, label, description, action }, ...] }
// 2. Transform (function receives defaults, returns new array)
toolbar: { row2: (defaults) => defaults.filter(b => b.id !== 'q') }
// Function form covers all operations via standard JS:
// - Append: (d) => [...d, newBtn]
// - Prepend: (d) => [newBtn, ...d]
// - Remove: (d) => d.filter(b => b.id !== 'q')
// - Replace: (d) => d.map(b => b.id === 'tmux-prefix' ? newBtn : b)
// - Insert: (d) => { const i = d.findIndex(b => b.id === 'tab'); return [...d.slice(0,i), newBtn, ...d.slice(i)] }
Floating buttons
Must use the grouped shape — a flat ControlButton[] is rejected:
floatingButtons: [
{
position: 'top-left', // required
direction: 'row', // optional: 'row' | 'column' (default 'row')
buttons: [{ id, label, description, action }],
},
]
Valid positions: top-left | top-right | top-centre | bottom-left | bottom-right | bottom-centre | centre-left | centre-right
Default button IDs
Toolbar row 1 (10 buttons):
id |
label |
action |
|---|---|---|
esc |
Esc | send \x1b |
tmux-prefix |
Prefix | prefix \x02 (sends prefix then opens combo picker for follow-up key) |
tab |
Tab | send \t |
shift-tab |
S-Tab | send \x1b[Z |
left |
<- | send \x1b[D |
up |
up arrow | send \x1b[A |
down |
down arrow | send \x1b[B |
right |
-> | send \x1b[C |
ctrl-c |
C-c | send \x03 |
enter |
enter | send \r |
Toolbar row 2 (7 buttons):
id |
label |
action |
|---|---|---|
q |
q | send q |
alt-enter |
M-enter | send \x1b\r |
ctrl-d |
C-d | send \x04 |
drawer-toggle |
hamburger More | drawer-toggle |
paste |
Paste | paste |
backspace |
backspace | send \x7f |
space |
Space | send ' ' |
Drawer (12 buttons):
id |
label |
action |
|---|---|---|
tmux-new-window |
+ Win | send \x02c |
tmux-split-vertical |
Split | | send \x02% |
tmux-split-horizontal |
Split -- | send \x02" |
tmux-zoom |
Zoom | send \x02z |
tmux-sessions |
Sessions | send \x02s |
tmux-windows |
Windows | send \x02w |
page-up |
PgUp | send \x1b[5~ |
page-down |
PgDn | send \x1b[6~ |
tmux-copy |
Copy | send \x02[ |
tmux-help |
Help | send \x02? |
tmux-kill-pane |
Kill | send \x02x |
combo-picker |
Combo | combo-picker |
Gestures
| Field | Default | Notes |
|---|---|---|
gestures.swipe.enabled |
true |
|
gestures.swipe.left |
'\x02n' |
Next tmux window |
gestures.swipe.right |
'\x02p' |
Previous tmux window |
gestures.swipe.threshold |
80 |
Pixels |
gestures.swipe.maxDuration |
400 |
Milliseconds |
gestures.pinch.enabled |
false |
|
gestures.scroll.enabled |
true |
|
gestures.scroll.strategy |
'wheel' |
'wheel' (recommended) sends SGR mouse wheel sequences — works in vim, less, htop. 'keys' sends PageUp/PageDown — simpler, works everywhere |
gestures.scroll.sensitivity |
40 |
|
gestures.scroll.wheelIntervalMs |
24 |
|
gestures.doubleTap.enabled |
false |
Opt-in double-tap gesture on terminal screen |
gestures.doubleTap.data |
'\x02z' |
Data to send on double-tap (default: tmux zoom toggle) |
gestures.doubleTap.maxInterval |
300 |
Max milliseconds between taps |
Font
| Field | Default | Notes |
|---|---|---|
font.family |
'JetBrainsMono NFM, monospace' |
CSS font-family |
font.cdnUrl |
jsdelivr nerdfont URL | CSS file for web font |
font.mobileSizeDefault |
16 |
px, applied on mobile |
font.sizeRange |
[8, 32] |
Min/max for +/- buttons |
PWA
| Field | Default | Notes |
|---|---|---|
pwa.enabled |
true |
Set false to disable manifest + icons |
pwa.themeColor |
'#1e1e2e' |
Status bar colour on mobile |
pwa.shortName |
(none) | Short name for home screen icon, falls back to name |
Hooks (advanced)
Hooks are programmatic, not via defineConfig(). See references/hooks.md if the user asks about analytics, action filtering, or custom DOM. Do not proactively suggest hooks during setup.
Escape-code cheat sheet
Use these in action.data and gesture left/right fields:
| Key | Escape sequence | Notes |
|---|---|---|
| Ctrl-B (prefix) | \x02 |
Default tmux prefix |
| Ctrl-A (prefix) | \x01 |
screen/byobu/custom prefix |
| Ctrl-C | \x03 |
Interrupt |
| Ctrl-D | \x04 |
EOF / exit shell |
| Escape | \x1b |
|
| Tab | \t |
|
| Shift+Tab | \x1b[Z |
|
| Enter | \r |
|
| Alt+Enter | \x1b\r |
|
| Backspace | \x7f |
DEL character |
| Up arrow | \x1b[A |
|
| Down arrow | \x1b[B |
|
| Right arrow | \x1b[C |
|
| Left arrow | \x1b[D |
|
| Page Up | \x1b[5~ |
|
| Page Down | \x1b[6~ |
|
| Space | ' ' |
literal space |
Composing tmux key sequences
tmux bindings are prefix + key. Concatenate the bytes:
Ctrl-B + c -> '\x02c' (new window)
Ctrl-B + n -> '\x02n' (next window)
Ctrl-B + p -> '\x02p' (previous window)
Ctrl-B + z -> '\x02z' (zoom pane)
Ctrl-B + % -> '\x02%' (split vertical -- stock tmux)
Ctrl-B + " -> '\x02"' (split horizontal -- stock tmux)
Ctrl-B + [ -> '\x02[' (copy mode)
Ctrl-B + d -> '\x02d' (detach)
For a custom prefix (e.g. Ctrl-A): replace \x02 with \x01.
Example configs
Minimal — default Ctrl-B prefix, custom name only
export default {
name: 'dev',
}
Custom prefix — Ctrl-A (screen/byobu style)
Replace the default tmux-prefix button and update swipe gestures:
export default {
name: 'dev',
toolbar: {
row1: (defaults) => defaults.map(b =>
b.id === 'tmux-prefix'
? { ...b, description: 'Send tmux prefix key (Ctrl-A)', action: { type: 'prefix', data: '\x01' } }
: b
),
},
gestures: {
swipe: {
left: '\x01n',
right: '\x01p',
leftLabel: 'Next tmux window',
rightLabel: 'Previous tmux window',
},
},
drawer: {
buttons: (defaults) => defaults.map(b => {
// Remap tmux-prefixed buttons from Ctrl-B (\x02) to Ctrl-A (\x01)
if (b.action.type === 'send' && b.action.data.startsWith('\x02')) {
return { ...b, action: { ...b.action, data: '\x01' + b.action.data.slice(1) } }
}
return b
}),
},
}
Agent watcher — auto-zoom + floating button
export default {
name: 'agents',
mobile: {
initData: '\x02z', // zoom focused pane on mobile load
},
floatingButtons: [
{
position: 'top-left',
buttons: [
{
id: 'zoom',
label: 'Zoom',
description: 'Toggle pane zoom',
action: { type: 'send', data: '\x02z' },
},
],
},
],
}
Scroll strategy — keys instead of wheel
export default {
gestures: {
scroll: { strategy: 'keys' },
},
}
Popup-heavy workflow — lazygit, yazi, scratch shell
Uses function form to keep default drawer buttons and append popup triggers:
export default {
name: 'dev',
drawer: {
buttons: (defaults) => [
...defaults,
{
id: 'lazygit',
label: 'Git',
description: 'Open lazygit popup (prefix + g)',
action: { type: 'send', data: '\x02g' },
},
{
id: 'yazi',
label: 'Files',
description: 'Open yazi file manager popup (prefix + y)',
action: { type: 'send', data: '\x02y' },
},
{
id: 'scratch',
label: 'Scratch',
description: 'Open scratch shell popup (prefix + `)',
action: { type: 'send', data: '\x02`' },
},
],
},
}
Requires matching tmux bindings (see references/tmux-basics.md popup section).
Guardrails
- Do not
importfrom'remobi'— the CLI callsdefineConfig()internally, so configs just export a plain object. Usingimport { defineConfig } from 'remobi'fails when the config lives outside a project with remobi installed. - Never invent root keys. The validator rejects unknown keys with a path-based error.
- Use
drawer.buttons, neverdrawer.commands— the latter was renamed and no longer works. sendactions requiredata— omitting it fails validation.- Non-
sendactions must not havedataorkeyLabel— validator rejects them. floatingButtonsis an array of groups — wrap buttons in{ position, buttons }.toolbarhasrow1androw2— there is norow3or flatbuttonskey on toolbar.mobile.initDataisstring | null— set tonullto disable, notfalseor''.reconnecthas onlyenabled: boolean— defaults totrue. Set{ enabled: false }to disable.gestures.scrollis an object, not a string — use{ strategy: 'wheel' }or{ strategy: 'keys' }, never a bare'wheel'/'keys'string.
Validation
remobi serve --port 18765 -- /bin/true
A zero exit means the config is valid when the file is in the normal search path (current directory or ~/.config/remobi/).
For a custom location, validate explicitly:
remobi serve --config /path/to/remobi.config.ts --port 18765 -- /bin/true
Any error output means fix the reported paths before proceeding.
Common validation errors
| Error | Cause | Fix |
|---|---|---|
config.<unknown-key> |
Invented or legacy root key | Remove it; only allowed root keys are valid |
config.drawer.commands |
Old key name | Rename to drawer.buttons |
config.toolbar.buttons |
Wrong toolbar shape | Use toolbar.row1 and/or toolbar.row2 |
action.type: expected 'send' | ... |
Wrong type string | Use exact literal from ButtonAction union |
action.data: expected string, received undefined |
send action missing data |
Add data: '\x...' |
action.data: expected undefined |
data on non-send action |
Remove data from non-send actions |
floatingButtons[0]: expected object |
Flat ControlButton[] |
Wrap in group: { position: 'top-left', buttons: [...] } |
mobile.initData: expected string or null |
false or 0 passed |
Use null to disable, or a string to send |
Cannot find package 'remobi' |
Config uses import ... from 'remobi' |
Remove the import — export a plain object instead. The CLI calls defineConfig() internally |
gestures.scroll: expected Object, received string |
Bare 'wheel' / 'keys' string |
Use { strategy: 'wheel' } or { strategy: 'keys' } |