@junyoung-kim/reins
v0.1.4
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.
- 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 |
|-----|--------|
| q | quit |
| r | reconnect to relay |
| n | new QR |
| c | show / hide the QR (URL text is always visible) |
| m | machines (list / add / edit / delete) |
| o | options (relay URL, port, timeouts) |
| i | info (read-only diagnostics) |
| a | toggle auto-start (run as a background service) |
| v | verbose log (↑ / ↓ to scroll) |
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).
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, withrrestart ·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. 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.
For developers
This repo is a pnpm monorepo (packages/*, apps/*):
ai_remote_vibe_agent/
├── apps/
│ ├── desktop/ @arva/desktop — Electron desktop host (GUI)
│ └── headless/ reins — Ink headless TUI host → has its own README
├── packages/
│ ├── core/ @arva/core — createAgentCore: the shared SSH/PTY/WS/relay engine
│ └── shared/ @arva/shared — protocol types · relay defaults (kept in sync with mobile)
└── docs/requests/ work requests / history (start from INDEX.md)@arva/core has no Electron dependency — it takes its config/identity via DI, so the
desktop (electron-store) and headless (JSON file) hosts inject their own backends. The
types in @arva/shared must stay 1:1 with the mobile app ([mobile-impact] commits).
pnpm install # whole workspace + desktop electron-rebuild
pnpm dev # desktop dev (watch)
pnpm typecheck # tsc --noEmit across packages
pnpm test # vitest across packages (headless TUI tested with ink-testing-library)
pnpm lint # biome check
pnpm --filter @junyoung-kim/reins build # headless → dist/cli.mjs (tsup)The workspace's
node-ptyiselectron-rebuild-ed for the desktop, so runningpnpm --filter @junyoung-kim/reins dev(plain Node) inside the workspace can crash with an ABI mismatch. Use the clean-install smoke procedure inapps/headless/README.mdto test the published form.
Releasing
- Desktop:
electron-builder, via GitLab CI (.gitlab-ci.yml, with the GitHubrelease.ymlkept as fallback). Auto-update uses a self-hosted (arv) generic feed. - Headless (reins):
pnpm --filter @junyoung-kim/reins publish.prepackbuildsdistand the tarball ships onlydist— no source, secrets, or native modules. Full steps inapps/headless/README.md.
Contribution rules (UI tokens, TSDoc, file-size limits, i18n, cross-platform) live in
CLAUDE.md and .claude/rules/.
Reference
README_KR.md— 한국어 버전apps/headless/README.md— reins run / env / release detailsCLAUDE.md·.claude/rules/— project guidelines & coding rulesdocs/requests/INDEX.md— work request / history index- Mobile side:
claude_code_mobile(Flutter) · Base template: https://github.com/daltonmenezes/electron-app
