@junyoung-kim/reins
v0.1.14
Published
Keep your AI coding agent on a leash, from your phone — a headless TUI (npx @junyoung-kim/reins)
Readme
reins — keep your AI coding agent on a leash, from your phone
Read in other languages: 한국어
You started Claude Code (or Codex / Gemini CLI) on your computer, then walked away. reins lets you watch that session, type into it, and approve what it does — from your phone. It hosts your computer's terminal and links it to the phone app through a cloud relay (or your LAN). Scan a QR once and you're paired.
npx @junyoung-kim/reinsThat's it. A pairing QR appears in your terminal — scan it with the phone app and your shell is now mirrored to your pocket.
What you get
- Mirror your terminal to your phone — see the live session, type, and approve the agent's actions remotely.
- Take it back at the keyboard —
reins attachputs the session you were driving from your phone right back in front of you, mid-command (below). - One command to host —
npx @junyoung-kim/reins, no GUI, no config files required. - Pair by QR — scan once. The QR is an HTTPS link, so a phone without the app installed lands on the install page instead of a dead end.
- Keep it running after you log out — register reins as a background service so the connection survives an SSH disconnect or a reboot (details).
- Manage machines and settings from the terminal — add/edit SSH or local machines, change the relay, all without leaving the TUI.
- A full desktop GUI too, if you prefer windows over a terminal (below).
Install & run
You need Node.js ≥ 18. Then, in the real terminal of the computer you want to host from:
npx @junyoung-kim/reins # run once, no install
# or
npm i -g @junyoung-kim/reins && reins # install globally, then just `reins`A pairing QR shows up. The pairing URL is also printed as plain text right below it, so you can pair even when a camera can't reach the screen (remote SSH, a tiny terminal window) — just open the URL on your phone.
Keys
| Key | Action |
|-----|--------|
| m | machines (list / add / edit / delete) |
| o | options (relay URL, port, timeouts) |
| i | info (read-only diagnostics) |
| d | get apps (desktop / mobile download links) |
| t | attach this terminal to a session (below) |
| y | copy the pairing ws URL |
| r | reconnect to relay |
| n | new QR |
| a | toggle auto-start (run as a background service) |
| U | update reins to the latest version |
| c | show / hide the QR (URL text is always visible) |
| v | verbose log (↑ / ↓ to scroll) |
| q | quit |
| PgUp / PgDn | scroll the whole screen when the terminal is too short |
The current screen always lists its own keys at the bottom, so you never have to remember this table.
Small terminals and split panes: reins keeps the current screen and relay status pinned and
shows ↑ / ↓ N · PgUp/PgDn when content does not fit. Form focus and confirmation dialogs are
revealed automatically; arrow keys continue to control the active list, log, or selector. A normal
80×24 terminal is recommended, but shorter terminals remain usable through paging. If a remote
keyboard has no Page keys, resize the pane as the universal fallback.
Pairing — how it connects
- reins starts a small local server and connects to the cloud relay as a host.
- It shows a QR — routed through the cloud once the relay is up, or a LAN
fallback QR before that. Either way it's an HTTPS pairing link
(
https://arv.juny-api.kr/pair?u=…), not a raw socket address. - Scan it with the phone app. Your machine list and terminals appear on the phone, and you can type and approve from there.
The default relay is wss://juny-api.kr/relay. You can point reins at your own relay
(see Configuration).
Take the session back — reins attach
You were driving the agent from your phone on the train. Now you're back at the keyboard.
reins attach puts that same session in front of you — same scrollback, same running
command, nothing restarted.
Straight from the reins screen — press t. On the machines screen (m), put the
cursor on one and press Enter. The TUI steps aside, that terminal becomes the session,
and Ctrl-] d brings the TUI back.
Or from another terminal:
reins attach # the only machine — or pick one, if there are several
reins attach mymachine # by name (or any unambiguous part of the id)
reins attach --list # what's availableThe screen redraws exactly as your phone last saw it, and you keep typing.
With more than one machine, attach shows a picker instead of making you copy a UUID. It tells you which sessions are actually live and who's holding them:
reins attach — pick a machine 5 machines · / to filter
macbook-pro local ● live
▸ dev-server ssh ● live phone is driving
raspberry-pi ssh ○ no session
build-box ssh ✕ error
feat/login-redesign worktree ● live another window
↑↓ move enter attach / filter q quit● live means there's already a screen to take over. ○ no session means attach will
start one. Press / and type to filter by name or id.
Handing it back to the phone — press Ctrl-] p, or just open the session on the
phone. This window drops to read-only and says so:
── phone took over ─────────────────────────────
Your keystrokes now go to the phone, not here.
T take it back Ctrl-] d quitPress T to take it back. If the phone was typing in the last 10 seconds, T asks once
more before pulling it away — the same rule the desktop app uses. Go back and forth as
often as you like.
It tells you how to leave, first thing. Attaching shows a short screen; press any key to drop into the session:
── dev-server — you are driving this terminal ──────────
Ctrl-] d detach — the session keeps running
Ctrl-] p hand the session back to the phone
Ctrl-] ? all keys
Ctrl-C goes to the program, not to reins.
press any key to continue n don't show this againPress n once you know the keys. If you later forget them, reins attach --guide brings
it back; --no-guide skips it just this once.
Ctrl-Cdoes not end attach. It goes straight to the program, so pressing it to leave stops the agent you were running, not reins. The key to leave isCtrl-] d. HitCtrl-Ca few times in a row and reins says so in one line.
Keys while attached
| Key | Action |
|-----|--------|
| Ctrl-] d | detach — leave the session running, give this terminal back |
| Ctrl-] p | hand the session to the phone |
| Ctrl-] r | redraw the screen |
| Ctrl-] ? | show these keys |
| Ctrl-] Ctrl-] | send a literal Ctrl-] to the shell |
Ctrl-]is vim's tag-jump. If you live in vim, pick another prefix —reins attach --escape ^A— or--escape noneto switch the prefix off entirely and send every key straight through.
If the agent restarts (reins service restart, or a self-update) the session doesn't
die. attach waits, reconnects, and redraws. Press q if you'd rather not wait.
When it won't connect
| Message | What to do |
|---------|-----------|
| no agent is running on this computer | Start one — reins, or reins service install |
| refused the attach handshake | That agent is older than your reins (no attach endpoint), or another program holds the port. Update the agent and restart it. |
| is already attached from another window | Detach there with Ctrl-] d, or take it: reins attach <machine> --force |
| the agent rejected this connection | The agent runs as a different user, so it reads a different session token. Run attach as that user (reins service status). |
If a crash ever leaves your terminal looking wrong — no echo, stuck colors, a cursor in
the wrong place — run reset. reins restores the terminal on every exit path it
controls, including Ctrl-C and a closed window, so you shouldn't need it often.
Exit codes, if you wrap attach in a script: 0 ok · 2 usage · 3 no agent ·
4 auth · 5 busy · 6 no such machine · 7 version mismatch · 8 session ended.
attach needs a real terminal (it won't run through a pipe), and it doesn't open a port of its own — so it's fine to run while
reinsis already running on the same machine. Windows isn't verified yet; if the prefix key or resizing misbehaves there,--escape noneis the workaround.
Keep it running after you log out
If you start reins over SSH and then close that SSH session, reins gets killed along with it — the host disappears and your phone can't reconnect. On Linux (systemd), reins can register itself as a background service that survives logout and reboot.
The easy way — press a in the TUI to turn auto-start on. (It enables boot start,
then hands off to the background service when you press q to quit, so you never run
two copies at once.)
Or from the command line:
npm i -g @junyoung-kim/reins # a one-off `npx` cache path can't be a stable service
reins service install # register + start in the background
reins service status # is it installed / active / enabled?
reins service uninstall # stop and removeA couple of things to know:
- Survive a full logout, not just reboot — run this once (needs root):
sudo loginctl enable-linger <your-user>.reins service installreminds you if it's needed. - Put your relay secret where the service can find it — the service reads it from
~/.ai_remote_vibe_agent/.env(see Configuration).reins service installwarns you if it's missing. - Run
reinsagain while the service is up and you won't start a second copy — reins opens a small manager screen instead, showing service status, recent logs, and the pairing QR, withtattach ·rrestart ·xstop ·udisable ·qquit.
macOS and Windows don't have this yet —
reins service …will tell you it's Linux-only for now and exit cleanly, without breaking anything.
Managing machines & options
reins is more than a viewer — you can manage everything from the terminal:
- Machines (
m) — list, add (SSH or local; key or password), edit, delete, andEnterto attach the highlighted one. reins shares the same machine registry as the desktop app, so changes show up on both sides, and live-update when the phone edits the list. - Options (
o) — change the relay URL (comma-separate for a fallback), the local port, and timeouts. A relay change reconnects right away; port / timeout changes apply on the next start. - Info (
i) — a read-only screen with the pairing URL, relay status, local server, agent identity, and system info. Your relay secret is only ever shown as set or missing — never the value.
Configuration
reins works out of the box. To connect through the cloud relay (rather than LAN only), give it a relay secret. To use a different relay, override the URL.
| Variable | What it does |
|----------|--------------|
| MAIN_VITE_RELAY_AGENT_SECRET (or VIBE_RELAY_AGENT_SECRET) | Relay auth secret. Without it, only the LAN-fallback QR works. |
| MAIN_VITE_RELAY_URL (or VIBE_RELAY_URL) | Use a different relay (comma-separate for fallbacks). Default: wss://juny-api.kr/relay. |
Set them as shell environment variables, or put them in a .env file:
# ./.env (next to where you run reins), or ~/.ai_remote_vibe_agent/.env
MAIN_VITE_RELAY_AGENT_SECRET=your-secret-herereins looks for the nearest .env walking up from the current directory, then falls
back to ~/.ai_remote_vibe_agent/.env. Your shell env always wins, and only the relay
keys above are read from .env (nothing else is injected).
Running as a service? The service starts from
/, so it can't find a.envnext to you — it reads only~/.ai_remote_vibe_agent/.env. Put your secret there.Trust note: because
.envis discovered from parent directories, anyone who can place a.envabove where you run reins can change where your secret is sent. Don't runnpx @junyoung-kim/reinsinside a directory tree you don't trust. The boot log prints the actualrelay targetso you can confirm.
Settings (machines, install id, FCM tokens) are stored at
~/.ai_remote_vibe_agent/. The secret is never written into the package or the service
unit — it stays in your environment.
Run your own relay server
reins pairs your phone and computer through a relay — a small WebSocket server
that forwards traffic between the host and the phone. The default is
wss://juny-api.kr/relay, but you can self-host your own in one command with
vibe-remote-relay:
# generate a shared secret and start the relay (WebSocket, port 8091)
RELAY_AGENT_SECRET=$(openssl rand -base64 32) npx vibe-remote-relayThe relay speaks WebSocket only (no built-in TLS), so put a reverse proxy in
front of it — e.g. Caddy: relay.example.com { reverse_proxy ws://localhost:8091 }.
Then point reins at it — set the base URL and the same secret (see Configuration):
# ~/.ai_remote_vibe_agent/.env
MAIN_VITE_RELAY_URL=wss://relay.example.com
MAIN_VITE_RELAY_AGENT_SECRET=your-secret-here # must equal the relay's RELAY_AGENT_SECRETreins appends /host/<token> to that base and authenticates with
Authorization: Bearer <secret>; the phone connects to /mobile/<token>.
The two packages
| Package | Use it when |
|---------|-------------|
| vibe-remote-relay | Stand up the relay — a CLI / Docker one-liner, or embed it as a NestJS module (RelayModule.forRoot(...)) inside an existing app. |
| vibe-remote-protocol | The v3 binary wire format (opcodes, encoders/decoders, message types) shared by relay, host, and phone — only needed if you build your own host or client. |
Full environment variables, Docker Compose, reverse-proxy configs (Caddy / nginx),
and the URL / auth contract live in the
vibe-remote-relay README.
Prefer a GUI? The desktop app
The same host also ships as a full desktop app (@arva/desktop, Electron + React) with
a terminal view, machine list, a cockpit, and settings. Headless reins and the desktop
app share the same engine and the same protocol — pick whichever fits the machine.
The phone side is a separate app (claude_code_mobile, Flutter) that talks to either
host over the same WebSocket relay / SSH.
Troubleshooting
The QR never appears / relay says disconnected. reins needs a relay secret to reach the cloud. Without one it still prints a LAN-fallback QR that works on the same Wi-Fi. See Configuration.
Phone paired, but the terminal is blank. Open the session again from the phone, or
press Ctrl-] r if you're attached — both force a full redraw.
reins service … says it's Linux-only. It is, for now. On macOS and Windows, keep
reins running in a terminal (or your own launchd / Task Scheduler entry).
Your terminal looks broken after a crash. Run reset.
Which reins am I actually running? If you have both a global install and a project
copy, they can disagree. reins --version prints the one on your PATH; the pairing
screen and i (info) show what the running host reports.
Source & contributing
reins is the headless host from the ai_remote_vibe_agent repo, which also builds the desktop app and shares one engine between them. Issues, build instructions, and contribution rules live there.
Reference
README_KR.md— 한국어 버전vibe-remote-relay— run your own relayvibe-remote-protocol— the wire format, if you're building your own client
