Native iPhone companion for Claude Code โ with Dynamic Island, multi-Mac pairing, and pinpoint terminal routing.
Lock screen Live Activity โ your pixel-cat companion shows the active session, the current phase, the last user message, and Claude's reply preview. Updates in real time via APNs.
Code Light is a native iPhone companion for Claude Code. It pairs with MioIsland on your Mac and lets you read, send, and orchestrate your AI coding sessions from anywhere โ without touching the keyboard.
Step away from your desk. Lock your phone. Get coffee. The pixel cat in your Dynamic Island will tell you the moment Claude needs you โ with the actual question on the lock screen, ready to tap and reply.
This is a passion project, purely for personal interest. It is free and open-source with no commercial agenda. Bugs, PRs and ideas are welcome.
"I already have Happy โ why would I use this?"
Short answer: because Code Light is built from the ground up for Claude Code + Mac + cmux, and it leans hard into native iOS. Every design decision picked correctness and feel over cross-platform breadth.
|
Your message lands in the exact Claude pane you picked โ not "the first Claude window I could find". Code Light walks |
A single global Live Activity reflects "whatever Claude is doing right now". Phase transitions ( |
|
Type |
Pair your phone with multiple Macs and switch between them with a tap. Each Mac has a permanent 6-character pairing code that never rotates โ no QR expiry, no "wait for the handshake", just "A7K2M9, go". Each Mac can live on a different backend server entirely. Sessions are strictly isolated per Mac. |
|
Tap |
Native SwiftUI, not a webview. Native Ed25519 via CryptoKit. Native |
Both apps let you talk to Claude Code from your phone. Here's where they actually differ:
| Capability | Code Light | Happy |
|---|---|---|
| Dynamic Island (real one, not a notification) | โ Global phase-driven activity | โ |
| Multi-Mac pairing (one iPhone โ many Macs) | โ Permanent short codes | โ One device at a time |
| Multi-backend-server (Macs on different servers) | โ Flat list, auto switch | โ |
| Precise cmux surface targeting | โ UUID โ PID โ env vars | โ No terminal routing |
Any Claude slash command (/model, /cost, /usage, โฆ) |
โ Captured output returned | โ Only text messages |
| Remote session launch (spawn new cmux pane from phone) | โ With Mac-defined presets | โ |
| Binary-efficient transport | โ Plain text + Socket.io frames | โ Base64-wrapped payloads |
| Native Swift iOS app | โ SwiftUI + ActivityKit | โ React Native / Expo |
| Rich markdown rendering (code, tables, lists, headings) | โ Custom SwiftUI renderer | |
| Terminal control keys (Esc, Ctrl+C, Enter) | โ Dedicated buttons + swipe | โ |
| Image attachments (camera + library) | โ Both | |
| Permanent pairing code (type instead of scan) | โ 6 chars, never rotates | โ QR only |
| In-app privacy policy (Apple-compliant) | โ Bilingual | |
| Full EN/ZH localization | โ Including Info.plist | |
| Self-hostable | โ | โ |
| Open source | โ CC BY-NC 4.0 (study + research, no commercial) | โ |
1. Terminal routing: no guessing game.
Happy has no concept of which cmux pane a message should go to. It can't, because it wraps your CLI (happy claude instead of claude) and only sees its own stdin/stdout. Code Light does the opposite: MioIsland on the Mac watches the whole system โ it knows every Claude process, every cmux surface, and the mapping between them via CMUX_WORKSPACE_ID/CMUX_SURFACE_ID env vars. A message targeted at session UUID abc12345โฆ lands in exactly that pane. If the process is gone, the message is cleanly dropped instead of hijacking a nearby window.
2. Binary transport, not base64.
Happy's wire format wraps payloads in base64 (Buffer.from(...).toString('base64') shows up all over their session routes). Base64 inflates every byte by 33% and requires an extra encode/decode round-trip on both sides. Code Light sends message content as plain UTF-8 strings over Socket.io frames โ smaller, faster, less code. Images are uploaded as raw binary via POST /v1/blobs and referenced by opaque IDs, never base64-blobbed into messages.
3. Real Dynamic Island, not a nudge. Code Light runs an ActivityKit Live Activity that reflects Claude's current phase in your iPhone's Dynamic Island โ not a push notification that disappears after 3 seconds. The activity updates in place as Claude moves between states, shows the active tool name, and collapses gracefully when all sessions finish. This is only possible because Code Light is a native Swift app. React Native can't do ActivityKit cleanly.
4. Slash commands round-trip.
/model, /cost, /usage, /clear etc. don't fire Claude's hook events โ they're handled inside the CLI and their output never reaches the JSONL. Most remote clients therefore can't see the response. Code Light's MioIsland bridge solves this by: snapshot the pane before injection, send the command, poll until output settles, diff the snapshots, ship the new lines back as a synthetic terminal_output message. From the phone it looks like any other response.
5. Multi-Mac really means multi-Mac.
Pair your phone with MacBook Pro and Mac mini, on two different servers if you want. Code Light shows both Macs in one list, grouped by server host, current connection marked green. Tap a Mac on a different server and Code Light reconnects in the background. Every Mac gets its own permanent 6-char shortCode (never expires) so pairing additional iPhones is just "type the code". Session access is strictly scoped per DeviceLink in the server DB โ Mac A's sessions are invisible to an iPhone paired only with Mac B.
6. Remote launch closes the loop.
From the phone, tap +, pick a preset like Claude (skip perms) + Chrome, pick a project from recent paths, tap Launch โ Code Light sends POST /v1/sessions/launch, the server emits a session-launch socket event scoped to your Mac's deviceId, MioIsland's LaunchService spawns cmux new-workspace --cwd <path> --command "<command>", and a fresh cmux workspace pops up running Claude. You never touched your keyboard. Presets are defined on the Mac (so you control the command whitelist), project paths sync from live session cwds.
Every message, tool call, and thinking block streams to your phone as it happens. Lazy-loaded history (50 per page, before_seq cursor for scrollback), delta sync on reconnect.
A global Live Activity with six states (thinking, tool running, waiting approval, writing, done, idle). Pixel-cat animation on the Mac matches the phone's state.
Text input, send with one tap. Dedicated buttons for Escape and Ctrl+C. All messages go straight to the target cmux pane via surface-ID routing.
/model, /cost, /usage, /clear, /compact, /help โ anything. Output is diffed from the terminal pane and shown in your chat view as a terminal_output bubble.
Attach photos via PhotosPicker or capture a new one with the camera. Up to 6 per message, JPEG-compressed locally, uploaded as blobs, pasted into the cmux pane on the Mac via NSPasteboard + AppleScript Cmd+V.
Each Mac's MioIsland menu shows a permanent 6-character code and a QR. On the iPhone, scan the QR or type the code โ either way triggers the same /v1/pairing/code/redeem endpoint. Multi-Mac: pair more by typing more codes. No accounts, no passwords, no re-pair after reboot.
Maintain a flat list of paired Macs across any number of backend servers. Tap to switch active connection. Each Mac carries its own serverUrl in local state.
Launch presets are defined on the Mac (name, command, icon, sort order). The phone fetches them, shows them in a sheet, and triggers cmux to create a new workspace with the chosen command and project path.
Per-device toggles: notify on completion, on tool approval wait, on error. APNs-delivered, Live Activity push via HTTP/2.
English and ็ฎไฝไธญๆ everywhere โ UI strings (Localizable.xcstrings), permission prompts (InfoPlist.xcstrings), and privacy policy. iOS auto-selects based on system language.
Carefully chosen feedback for every class of interaction: selection for tab/picker, light for navigation, medium for buttons, rigid for commits, success for pair/launch, warning before destructive actions, error for failures.
Mac (MioIsland) Backend (self-hosted) iPhone (Code Light)
โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Claude Code โ โ Fastify + Socket.io โ โ ๐ฑ Linked Macs list โ
โ hooks + JSONL โ โ PostgreSQL + Prisma โ โ ๐ฌ Chat + markdown โ
โ โ โ โ โ ๐๏ธ Dynamic Island โ
โ MioIsland โโโโโโโโโโถโ DeviceLink graph โโโโโโโถโ โจ๏ธ Send + control keys โ
โ ยท SessionStore โ WebSocketโ Zero-knowledge relay โ WSS โ ๐ท Camera + photos โ
โ ยท LaunchService โ + HTTPS โ APNs bridge (HTTP/2) โ โ ๐ Remote launch โ
โ ยท PresetStore โ โ โ โ ๐ Push notifications โ
โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโ
cmux bridge ActivityKit
(workspace + surface env vars) WidgetKit
Strict per-device isolation via DeviceLink in the server DB. An iPhone paired only with Mac A cannot see Mac B's sessions, presets, projects, or launch endpoints.
- Mac: macOS 14+, MioIsland installed, cmux for terminal integration
- iPhone: iOS 17+
- Server: any host with Node.js 20+ and PostgreSQL 14+ (or use a public CodeLight Server)
git clone https://github.com/MioMioOS/MioServer.git
cd MioServer
npm install
cp .env.example .env
# Set DATABASE_URL, MASTER_SECRET (64-char hex), PORT
npx prisma db push
npx tsx --env-file=.env ./sources/main.tsPut Nginx in front with TLS. pm2 start npm --name codelight-server -- start for production.
Follow the MioIsland README. Its Sync module will auto-register this Mac with your server and lazy-allocate a permanent 6-character pairing code.
cd CodeLight/app
open CodeLight.xcodeprojSelect your development team โ connect your iPhone โ press โR.
- On your Mac, open MioIsland's notch menu โ Pair iPhone. You'll see a QR and a 6-char code.
- On your iPhone, enter your server URL and the code (or scan the QR).
- Done. The Mac appears in the "Macs" list. Tap in to see its sessions.
| Layer | How |
|---|---|
| Identity | Ed25519 keypair (CryptoKit), per-device, never exported |
| Storage | iOS/macOS Keychain |
| Transport | TLS 1.2+ (HTTPS + WSS) |
| Pairing | Per-Mac permanent 6-char shortCode, unique server-side |
| Access control | DeviceLink graph โ every request checks getAccessibleDeviceIds() |
| Messages | E2E-encryption-ready (CryptoKit ChaChaPoly), zero-knowledge relay |
| Data collection | None. No analytics. No telemetry. No third parties |
See Privacy Policy. The policy is also viewable inside the iPhone app without a network connection (Apple App Store requirement).
CodeLight/
โโโ server/ # Fastify + Socket.io + Prisma backend
โโโ app/
โ โโโ CodeLight/ # Main iPhone app (SwiftUI)
โ โโโ CodeLightWidget/ # Dynamic Island / Lock Screen widget
โโโ packages/
โ โโโ CodeLightProtocol/ # Shared DTOs (Codable)
โ โโโ CodeLightCrypto/ # Ed25519 + ChaChaPoly
โ โโโ CodeLightSocket/ # Socket.io Swift wrapper
โโโ docs/specs/ # Design docs (multi-mac pairing, etc.)
A handful of non-obvious design decisions that make the system feel solid in practice.
Two facts the system already knows:
- Claude Code is invoked with
--session-id <UUID>on argv (ps -Ax) - cmux exports
CMUX_WORKSPACE_ID/CMUX_SURFACE_IDinto every pane (ps -E -p <pid>)
Pipeline: ps โ PID โ env vars โ cmux send --workspace <ws> --surface <surf>. No title matching, no cwd heuristics. Orphan processes are cleanly dropped.
Running one activity per session made the Dynamic Island stretch/collapse as sessions came and went, and hit iOS concurrent-activity caps. Code Light runs one global activity whose ContentState carries activeSessionId, activeSessions, totalSessions, latest phase. Whoever had the most recent phase change owns the island. Switching context is a state update, not a lifecycle cycle.
Only type: "phase" messages re-render the Live Activity. Regular chat messages don't. This keeps APNs push volume under Apple's budget and stops the Dynamic Island from flickering when Claude writes a long reply.
Node's built-in fetch() uses HTTP/1.1 and fails against api.push.apple.com with an opaque TypeError: fetch failed. The server hand-rolls node:http2 for Live Activity updates. Regular alert pushes use fetch() because Apple accepts both there; only Live Activity demands HTTP/2.
Phone sends โ server broadcasts โ MioIsland pastes โ Claude writes to JSONL โ file watcher sees it โ MioIsland re-uploads โ phone gets its own message back. Fixed with a 60 s TTL (claudeUuid, text) ring on the Mac: MessageRelay consumes a matching entry before uploading and skips. No server changes, no localId negotiation.
Images are transit cargo (the real history lives in Claude's JSONL once pasted), so the blob store is deliberately memory + disk, never DB. Three-tier cleanup: blob-consumed socket ack deletes on pickup, 10-minute TTL sweeper catches the rest, server startup purges blobs/ on every boot. No Prisma model, no orphan rows.
cmux has no "paste image" command. But Cmd+V with an image on the clipboard works. So: download blob โ cmux focus-panel โ AppleScript activate cmux and poll NSWorkspace.frontmostApplication until true โ write image to NSPasteboard in NSImage + public.jpeg + .tiff for max terminal compatibility โ System Events keystroke "v" using {command down} (with CGEvent fallback) โ cmux send for trailing text. Needs Accessibility permission, and the permission is tracked by signed path so MioIsland auto-installs itself to /Applications/Code Island.app to survive rebuilds.
shortCode is a column on Device, not PairingRequest. Lazy-allocated on first POST /v1/devices/me {kind:"mac"}, never rotated. Restarting MioIsland does not change your pairing code. Multiple iPhones can pair with the same Mac by redeeming the same code.
Launch presets use Mac-generated UUIDs as primary keys on the server, not server cuids. The Mac sends its local UUIDs on upload; the server stores them as-is. When the phone later sends a session-launch event with presetId, the Mac's local PresetStore can look it up immediately. Avoided a subtle "unknown presetId" bug during early phase 4 testing.
- Permission approval from phone (tap to allow tool use)
- Tool result visualization (file diff, terminal output)
- Chat history search
- iPad layout
- Android port (community-driven โ the protocol is cross-platform)
| Project | Role |
|---|---|
| MioIsland | Required โ the Mac-side bridge |
| MioServer | Required โ self-hosted relay server |
| cmux | Recommended โ the terminal multiplexer that makes precise routing possible |
Bug reports, PRs, and feature ideas all welcome.
- Report a bug โ Open an issue
- Submit a PR โ Fork, branch, code, PR
- Propose a feature โ Open an issue tagged
enhancement
- Email โ xmqywx@gmail.com
- Issues โ https://github.com/MioMioOS/CodeLight/issues
CC BY-NC 4.0 โ free for personal, educational, academic, and research use. Commercial use is not permitted. This includes (but isn't limited to) selling the app, bundling it into a paid product, or using it in a paid service.
For commercial licensing inquiries, contact xmqywx@gmail.com.




