Blog
Running Claude Code Remotely from Your Phone
How Tactic Remote turns your Mac into a reliably available AI coding server and your iPhone into a first-class control surface — with concrete architecture details.
Follow product and engineering updates from this channel.
Browse categoryClaude Code is a terminal-based AI coding assistant. It runs on your Mac, reads your codebase, writes code, runs tests, and commits changes. The problem: you have to sit at your Mac while it works.
Tactic Remote exists to break that constraint. Your Mac stays the execution engine. Your iPhone becomes the control surface. This article covers exactly how that works — the architecture, the protocol, the trade-offs, and the setup. No hand-waving.
TL;DR: A Node.js WebSocket server on your Mac manages Claude Code through tmux. Your iPhone connects over LAN or Cloudflare Tunnel, sends commands, and receives streamed terminal output. Push notifications alert you when Claude finishes or needs input.
The Architecture
Four components, three network paths:
The Node.js server has exactly one runtime dependency: the ws npm package. Everything else — HTTP server, authentication, rate limiting, tmux management, file browsing — is built on Node.js built-ins. This is deliberate. Fewer dependencies means fewer things that break, fewer supply chain risks, and a smaller attack surface.
How the Server Controls Claude Code
Here's the part that surprises most people: there is no API between the server and Claude Code. The server controls Claude Code the same way you do — by typing into a terminal.
Claude Code runs inside a tmux session. The server sends keystrokes with tmux send-keys and reads output with tmux capture-pane. It's terminal automation, not an SDK integration.
Creating a Session
When you create a new session from your iPhone:
The -l flag on send-keys is critical — it tells tmux to interpret the content literally, preventing special characters from being treated as tmux key names.
The 50,000-line scrollback buffer is 25x the tmux default. Claude Code sessions produce enormous output — compilation logs, file diffs, test suites. Without this, you'd lose context after a few minutes of heavy work.
Sending a Message to Claude
When you type a prompt on your iPhone and hit send:
The C-u at the start clears the input line. Without it, if Claude had left partial text in the prompt (from a previous interrupted input), your message would append to garbage.
Stopping Claude
Stopping is a multi-step sequence because Claude Code has two states that need different handling:
If Claude is mid-execution, Ctrl+C interrupts it. If Claude is already at its prompt, /exit is Claude Code's quit command. Sending both covers both states. The 200ms sleeps give Claude time to process each signal before the next arrives.
Terminal Output Streaming
This is the hardest engineering problem in the entire system. Claude Code outputs text at hundreds of lines per second during compilation or test runs. That output needs to reach your iPhone in real time, without draining the battery or overwhelming the network.
The Polling Loop
The server captures terminal content using a 1-second polling interval:
This grabs the last 2,000 lines of scrollback as plain text with ANSI escape codes preserved.
Why polling instead of event-driven? tmux doesn't offer change notifications. We evaluated filesystem watchers on tmux's internal socket — fragile across macOS versions. Polling at 1 second is a good balance: fast enough that output feels real-time, slow enough to minimize CPU usage when the session is idle.
Stability Detection
The output monitor doesn't just stream raw captures. It runs a stability detection algorithm:
- Capture terminal content every second
- Compare against the previous capture
- If different: send the update to the iPhone, reset the stability counter
- If identical for 5 consecutive captures (5 seconds stable): check if Claude is waiting for input
- If Claude is at its prompt: send a final update with
final: true, then stop polling
This handles Claude's progressive rendering — during long outputs, the terminal changes rapidly. After Claude finishes, the screen stabilizes. Five seconds of stability is a reliable signal that Claude is done.
The monitor has a hard cap of 120 checks (2 minutes). If output is still changing after 2 minutes, the monitor stops and the client can request a manual refresh.
How We Detect Claude's State
The server screen-scrapes Claude Code's TUI to determine what it's doing. This is necessarily heuristic:
We check the last few lines of terminal output for these patterns. If we see Goodbye! or Session ended, Claude has exited. If we see a shell prompt ($ at end of line) without Claude-specific indicators, the session has dropped back to the shell.
Is this fragile? Somewhat. If Claude Code changes its TUI layout, our heuristics break. But the alternative — building a custom IPC channel into Claude Code — would require maintaining a fork and tracking upstream changes. Screen-scraping the existing TUI is pragmatic: it works with every version of Claude Code without modification.
ANSI Color Preservation
A key design decision: the server preserves ANSI escape codes in the output. It strips problematic terminal sequences (cursor movement, OSC title sequences, private mode changes) but deliberately keeps color codes intact:
The iOS app then parses these codes into native AttributedString styling. This gives you Claude Code's full color output — syntax highlighting, diff coloring, status indicators — faithfully rendered on your phone screen. The parser supports standard 16 colors, 256-color mode, and 24-bit true color.
The WebSocket Protocol
The server speaks two protocols simultaneously on the same WebSocket connection — a legacy flat protocol and a newer request/response/event protocol.
New Protocol (Recommended)
Three frame types:
The id field enables proper async request multiplexing. The client can have multiple in-flight requests and match responses correctly. The legacy protocol has no request IDs — responses are positional, and if you send two requests quickly, you can't tell which response belongs to which request.
Complete Method Catalog
| Method | Purpose |
|---|---|
list_sessions | Get all tmux sessions with names, paths, attach status |
create_session | Create a new tmux session at a given path |
attach_session | Switch to an existing session, get its current output |
delete_session | Kill a tmux session immediately |
kill_session | Gracefully stop Claude, then kill the session |
start_claude | Launch Claude Code in the current session |
stop_claude | Send Ctrl+C + /exit sequence |
send_to_claude | Type a message and press Enter |
send_key | Send a single key (Enter, Escape, Tab, y, n, Ctrl+C) |
get_output | Get current terminal content and Claude's state |
get_full_output | Get up to 5,000 lines of scrollback |
change_path | Change the working directory |
list_directory | Browse files at a path |
create_folder | Create a new directory |
The send_key method has an explicit allowlist of 18 keys. You can't send arbitrary key sequences — only keys that make sense for interacting with Claude Code's interface. The y and n keys are specifically included for answering Claude's permission prompts from your phone.
Network: LAN vs. Cloudflare Tunnel
Two connection modes solve two different problems.
Local Network (LAN)
Your iPhone connects directly to your Mac over WiFi:
The Mac companion app publishes a Bonjour service (_ws._tcp.) so your iPhone can auto-discover it on the network. No configuration needed — open the app, tap the banner, you're connected.
Latency: Sub-10ms round trip. Terminal streaming feels instantaneous.
Limitation: Only works on the same network. Step outside your house and you lose the connection.
Cloudflare Tunnel
For remote access, the Mac companion app can start a Cloudflare Tunnel:
The tunnel is outbound-only from your Mac. No ports are opened on your firewall. No router configuration. Cloudflare handles TLS termination, DDoS protection, and WebSocket proxying.
Quick tunnel setup generates a random *.trycloudflare.com subdomain and a random 32-character API key:
No Cloudflare account required. Free. Works in about 10 seconds.
Latency: 30–80ms typical, depending on your distance to the nearest Cloudflare edge.
Trade-off: The URL changes every time you restart the tunnel. For a persistent URL, you need a Cloudflare account and a named tunnel (still free).
QR Code Quick Connect
Setting up a connection manually means entering an IP address, port, and API key on a phone keyboard. This is error-prone and annoying.
The Mac companion app generates a QR code containing the connection details as JSON:
Scan it from the iPhone app using the Vision framework's barcode detector. One scan sets up the entire connection — mode, address, authentication. Takes about 3 seconds.
Notification Pipeline
Push notifications are Tactic Remote's killer feature for "set it and forget it" workflows. Start a long task, walk away, get notified when Claude finishes or needs your input.
How It Works
Claude Code supports hooks — shell scripts that execute on lifecycle events. The Mac companion app auto-configures these hooks to POST to the local server:
When Claude finishes a task, this hook fires. The server receives the POST, broadcasts a notification event to all connected clients, and the iOS app fires a local notification via UNNotificationCenter.
Event Deduplication
Hook events can fire in rapid succession (e.g., Claude stops and restarts multiple subtasks). The iOS app deduplicates events using a 3-second window keyed by event|project|session|timestamp. If the same logical event arrives twice within 3 seconds, the duplicate is silently dropped.
Foreground vs. Background
When the app is in the foreground, notification events append a visual indicator to the terminal buffer:
When the app is in the background, a system notification fires. Tap it to jump directly back into your session.
Security Model
Running AI-generated code remotely demands a serious security model. Here's ours.
Authentication
The server supports API key authentication via two mechanisms:
Authorization: Bearer <key>header (standard)Sec-WebSocket-Protocol: <key>header (fallback for iOS WebSocket libraries that can't set custom headers on the upgrade request — a common mobile constraint)
Authentication is checked once at connection time. If it fails, the WebSocket is closed immediately with code 1008 (Policy Violation).
Command Injection Prevention
Every external command the server executes goes through execSafe(), which calls child_process.spawn() with shell: false. Arguments are passed as arrays rather than interpolated into shell command strings. This materially reduces shell injection risk.
Input validation adds defense in depth:
| Input | Validation |
|---|---|
| Session names | /^[a-zA-Z0-9_-]{1,64}$/ |
| Claude command | /^claude(\s+--?[a-zA-Z0-9-]+)*$/ |
| Key names | Explicit allowlist of 18 keys |
| File paths | Resolved to absolute, checked with startsWith() |
Path Traversal Protection
File browsing is restricted to the user's home directory tree. Sensitive directories are blocked regardless of other settings:
The check uses segment-by-segment matching (not substring matching), so a directory named my-.ssh-backup won't trigger a false positive.
Rate Limiting
Failed authentication attempts are rate-limited to 30 per minute per IP using an in-memory sliding window. After exceeding the limit, the server returns 429 Too Many Requests with a retryAfter hint.
No Cloud Dependency
Tactic Remote is a control plane, not a vendor cloud data plane. In the default architecture, the WebSocket carries terminal text between your Mac and iPhone. Source files, git history, and build artifacts are not uploaded to Tactic Remote services.
When using Cloudflare Tunnel, the tunnel encrypts traffic end-to-end. Cloudflare sees encrypted WebSocket frames, not your code.
The iOS App: Not a Terminal Emulator
Early in development, we made a conscious decision: the iPhone app is not a dumb terminal emulator. Cramming a full 80x24 terminal onto a 4-inch phone screen is a terrible experience.
Instead, the app provides:
- Conversation-style input: Type messages to Claude in a text field, not a terminal prompt
- Terminal output view: Rendered text with ANSI color support, auto-scrolling, pinch-to-zoom font size (default 8pt)
- Quick action buttons: Enter, Escape, Tab, Y, N, Ctrl+C — the keys you actually need when interacting with Claude Code
- Session picker: Browse and switch between tmux sessions
- Path browser: Navigate your Mac's filesystem to set the working directory
- Slash commands: Auto-complete for
/help,/clear,/compact,/cost,/model,/status
Reconnection Strategy
Mobile apps live in a hostile network environment. Your phone switches between WiFi and cellular, enters elevators, goes to sleep. The app's reconnection strategy handles all of this:
- Connection drops detected: WebSocket close event fires
- Exponential backoff: Retry after
min(attempts * 3, 15)seconds, up to 5 attempts - Background awareness: If the app is backgrounded, skip reconnection. Set a flag to reconnect when foregrounded
- Session restoration: On reconnect, list sessions → find the previously active session → reattach → poll for output at 0.2s, 0.7s, and 1.5s delays
The staggered output polling (0.2s, 0.7s, 1.5s) after reconnection handles the case where tmux needs a moment to produce a capture after reattachment. A single immediate poll would often return empty content.
Terminal Buffer
The TerminalBuffer is a circular buffer capped at 500,000 characters and 10,000 lines. When it overflows, content is truncated from the front, snapping to the nearest newline boundary to avoid showing a partial first line.
This cap exists because AttributedString rendering gets expensive at scale. 500K characters is roughly 30 minutes of heavy Claude Code output — enough for any reasonable session while keeping the UI responsive.
The Mac Companion App
The Mac companion app is a menu-bar application that manages the Node.js server, Cloudflare tunnel, and system settings.
Server Bundling
The entire Node.js server is bundled inside the Mac app. On first launch, it copies the server files to ~/Library/Application Support/ClaudeCodeRemote/Server/. On subsequent launches, it compares files byte-by-byte and only re-copies changed files. npm install only re-runs if package.json has changed.
This means most users do not need to touch the terminal to install or update the server.
Dependency Detection
The app checks for required tools (Node.js, tmux, Homebrew) by scanning known paths first (/opt/homebrew/bin/, /usr/local/bin/), then falling back to which. If dependencies are missing, a setup screen guides the user through installation.
Sleep Prevention
When the server is running, the app calls IOPMAssertionCreateWithName to prevent macOS from sleeping. This is essential — a sleeping Mac can't serve WebSocket connections.
Hook Auto-Configuration
The companion app writes shell scripts to ~/.claude/hooks/ that POST lifecycle events to the local server. This happens automatically on server start — no manual configuration needed.
What We Didn't Build
Some things we deliberately chose not to do:
No terminal multiplexing in the app. We use tmux on the server side, but the iPhone app shows one session at a time. Split panes on a phone screen would be unreadable.
No file editing. The app can browse files but not edit them. Claude Code handles file editing — you're the reviewer, not the editor. Adding a mobile code editor would be scope creep that degrades the core experience.
No offline mode. If your Mac is unreachable, the app shows you the last known state but can't do anything useful. We don't pretend otherwise.
No remote desktop fallback. Screen sharing and VNC exist, but they're 10x worse for this use case. Tactic Remote sends structured text, not pixels. This means lower bandwidth, lower latency, and a UI designed for the task rather than a shrunken desktop.
Quick Setup
Skip the details? Here's the minimum to get running.
On Your Mac
- Install the Mac companion app from tacticremote.com/download
- Launch it — it auto-installs dependencies and starts the server
- Note the QR code displayed in the app
On Your iPhone
- Install Tactic Remote from App Store
- Scan the QR code from the Mac app
- Create a session pointing to your project directory
- Start Claude and send your first prompt
For remote access (outside your home network), enable the Cloudflare Tunnel toggle in the Mac app's settings. The app generates a public URL automatically.
Configure Notifications
In the Mac app, hooks are configured automatically when the server starts. On the iPhone, grant notification permissions when prompted. That's it.
FAQ
Does Claude keep running if I close the iPhone app?
Yes. Claude runs in tmux on your Mac. The iPhone app is a window into that session. Close the app, put your phone in your pocket — Claude keeps working. Open the app again and you'll see everything that happened while you were away.
What if my network drops?
The iPhone app reconnects automatically with exponential backoff (up to 5 attempts over ~45 seconds). On reconnect, it reattaches to your previous session and fetches the accumulated output. No work is lost — tmux buffers everything on the Mac side.
How much battery does this use?
The iPhone app does not maintain a persistent background connection. When backgrounded, it disconnects cleanly. Battery impact is essentially zero when you're not actively using it.
Can I use this with other AI coding tools?
Tactic Remote is built around Claude Code's terminal interface, but the underlying architecture (tmux session management, terminal streaming) works with any terminal-based tool. If it runs in a terminal, the server can manage it. The state detection heuristics are Claude Code-specific, but the basic send-keys/capture-pane loop is universal.
Is my code safe?
Tactic Remote itself does not upload your source code to Tactic Remote-managed cloud services. It transmits terminal text over encrypted WebSocket connections. The server has no Tactic Remote telemetry pipeline by default. When using Cloudflare Tunnel, traffic is encrypted in transit and Cloudflare proxies transport frames rather than your local repository files.
If you want to dig deeper into specific subsystems, see Why We Chose tmux as Our Session Backbone, Building Real-Time Terminal Streaming, and the Security Model documentation.
Try Tactic Remote
Control your coding Agents from your phone
Connect to Claude Code, Codex, and other Agents on your Mac, Windows, or Linux computer. Check progress and send the next instruction from iPhone or iPad.