remi [args]
Start Claude Code with transparent remote monitoring.
Wrapper Mode (Default)
remi [options] [claude-args...]
Remi spawns Claude Code in the foreground and passes your terminal through directly. You interact with Claude as usual. In the background, Remi:
- Starts a WebSocket server for remote clients (default port 18765)
- Starts a hook HTTP server on an OS-assigned port and writes hook configuration to
.claude/settings.local.json - Connects to the signaling relay for code-based access
- Receives structured events from Claude Code via hooks (tool use, permission prompts, status changes)
- Writes status to
~/.remi/status-<port>.jsonfor the Claude Code status line
All wrapper-mode logs go to ~/.remi/remi.log so they never interfere with your terminal.
Examples
# Start in current directory
remi
# Pass arguments to Claude Code
remi my-project
# Resume a previous session
remi --resume
# Custom port
remi --port 9000 my-project
# Disable relay (local-only)
remi --no-relay my-project
# Localhost-only (no network exposure, no mDNS)
remi --local my-project
The --local flag is a shorthand for --bind localhost --no-mdns. It binds only to localhost (no network exposure) and disables mDNS advertising. Useful when you only need local browser access or SSH tunnel connections.
Daemon Mode
remi --daemon
Runs Remi as a headless server without a local terminal. Claude sessions are created on demand when a remote client connects. Output goes to the console instead of the log file.
Daemon mode is used with --install for running Remi as a persistent service.
Options
Server options
| Option | Description | Default | Env Var |
|---|---|---|---|
--port PORT | WebSocket server port | 18765 | REMI_PORT |
--bind HOST | Bind WebSocket server to HOST | 0.0.0.0 (all interfaces) | |
--daemon | Headless daemon mode | off | |
--local | Localhost-only mode (same as --bind localhost --no-mdns) | off | |
--orphan-timeout SECS | How long orphaned sessions stay alive when no client is connected. Set to 0 to disable cleanup. | 300 (5 minutes) | |
--max-bullet-length N | Truncate message bullets longer than N characters (0 to disable) | 500 | REMI_MAX_BULLET_LENGTH |
Relay and network options
| Option | Description | Default |
|---|---|---|
--no-relay | Disable signaling relay (no remote code access) | off |
--no-mdns | Disable mDNS network advertising | off |
--no-telegram | Disable Telegram adapter | off |
--signaling-url URL | Custom signaling server URL | wss://remi-signaling.yooz.workers.dev/connect |
--permanent-code | Use a persistent connection code (requires Ed25519 auth for relay connections) | off |
--network | Discover sessions across the local network (for ls) | off |
Authentication options
| Option | Description | Default |
|---|---|---|
--auth | Force enable authentication | auto (enabled for 0.0.0.0) |
--no-auth | Disable authentication | off |
--no-tofu | Reject unknown clients (disable Trust On First Use) | off |
Subcommand-specific options
See also Global Options for --host, --dir, and --recent.
| Option | Description |
|---|---|
--force | Overwrite existing identity (for keygen, import-key) |
--passphrase | Encrypt identity with a passphrase (for keygen) |
--decrypt | Remove passphrase from existing identity (for keygen) |
--encrypt | Add passphrase to existing identity (for keygen) |
--label NAME | Label for authorized key (for authorize) |
--public-only | Export only public key (for export-key) |
--remove FINGERPRINT | Remove authorized key by fingerprint (for authorize) |
In wrapper and daemon mode, when authentication is enabled (the default for network binds), Remi needs to unlock your identity at startup. If your identity was created with --passphrase, you will be prompted for the passphrase (or set REMI_PASSPHRASE). Unencrypted identities (the default) unlock automatically. Subcommands like ls, attach, recent, and kill do not require identity unlocking.
How It Works
In wrapper mode, Remi uses Bun's native PTY support to spawn Claude Code. Raw terminal bytes pass through to your terminal unchanged, preserving colors, cursor movement, and all ANSI escape sequences.
Hook-Based Event Detection
Before spawning Claude Code, Remi writes hook configuration into the project's .claude/settings.local.json. This configures Claude Code to POST structured JSON events to Remi's local hook server (http://127.0.0.1:<auto-port>/hooks).
Remi registers handlers for the full set of Claude Code hook events. The ones that change user-visible state:
| Hook Event | What Remi Does |
|---|---|
SessionStart | Captures Claude's session ID and transcript path; locks on first match, detects restart vs foreign via PTY liveness |
SessionEnd | Resets tracker state so a subsequent SessionStart cleanly takes over |
PreToolUse | Sets status to "executing" with the tool name; tracks Task/Agent nesting for subagent filtering |
PostToolUse | Sets status to "thinking"; decrements subagent nesting depth |
Notification (permission_prompt) | Creates a Yes/No question and sets status to "waiting" (suppressed when a prior PermissionRequest already emitted one) |
Notification (idle_prompt) | Sets status to "idle" |
PermissionRequest | Authoritative permission prompt with tool context and suggestions; feeds auto-approve when enabled |
Stop | Sets status to "idle" (when the agent turn actually ended) and clears stale subagent tracking |
StopFailure | Emits a retry question to the user |
SubagentStart / SubagentStop | Status context for synchronous subagent runs |
PostToolUseFailure | Surfaces tool failures in status |
Accepted but currently just forwarded to dynamic listeners (no default status change): UserPromptSubmit, InstructionsLoaded, TaskCompleted, TeammateIdle, ConfigChange, WorktreeCreate, WorktreeRemove, PreCompact, PostCompact, Elicitation, ElicitationResult. All 22 registered Claude Code hook events reach the server with a 200 response so Claude Code never gets blocked.
Subagent and team-member filtering
Claude Code tags hook events that originate from a background subagent or a team member with an agent_id field. Remi drops those events at the hook layer so subagent permission prompts never become phone notifications, never trigger auto-approve, and never inject keystrokes into the main PTY. See Auto-Approve guide — Multi-session behavior.
Session lock and restart detection
Remi treats its own PTY process as the ground truth for "main interactive session." When a hook event arrives with a different session_id than the current lock:
- PTY still running + no explicit
SessionEnd→ event is foreign (subagent or sibling daemon), dropped. - PTY exited OR
SessionEndreceived → event is a genuine Claude restart, lock transitions to the new session.
/clear, /compact, and /resume emit SessionStart with a known source value; Remi pre-sets the ended flag so those transitions are recognized as lifecycle events rather than foreign events.
This replaces the earlier approach of parsing terminal output. Hooks provide structured, reliable event data directly from Claude Code, with no risk of ANSI escape sequence interference.
On shutdown, Remi removes its hook entries from .claude/settings.local.json, leaving any other settings intact.
Debug capture
Set REMI_HOOK_DEBUG=1 before starting Remi to dump every raw hook POST to ~/.remi/hook-diag.jsonl. Each line is a JSON object with a _ts timestamp and the raw payload. Useful for investigating hook routing issues.
Port Conflicts
In wrapper mode, if the WebSocket port is already in use, Remi logs a warning to ~/.remi/remi.log and continues. Claude Code works normally; only remote monitoring is disabled.
In daemon mode, a port conflict causes Remi to exit with an error.