Code From Anywhere: Remote Development With Your iPhone and Claude Code
Published on January 24, 2026 · Updated January 24, 2026
Last week I found myself at a coffee shop with two hours to kill and only my iPhone. I had a feature I wanted to build. So I pulled out my phone, opened Blink, connected to my Mac at home, and started coding.
Not "reviewing code" or "checking logs" - actual development. Writing code with Claude Code, running my dev server, viewing the app in Safari, iterating. The full feedback loop, from a 6-inch screen.
This post is the complete setup guide. If you've ever wanted to code from literally anywhere - your phone, a tablet, a borrowed laptop - this is how.
What We're Building
The goal is simple: code it, run it, see it - from your phone.
Here's the workflow:
1. iPhone (Blink) → SSH tunnel → Mac → tmux → Claude Code
2. Start any dev server (Python, Node, Rails, etc.) on localhost
3. ngrok exposes it → View on your phone's browser
4. See changes, iterate, repeat
The infrastructure:
- ngrok tunnels - Expose your Mac's SSH and dev servers to the internet securely
- Traffic policies - Block bots, scanners, and rate limit requests at the edge
- LaunchAgents - Auto-start everything on boot
- Watchdog - Automatically restart if anything goes down
- tmux session - Persistent Claude Code session you can attach to from anywhere
Once set up, you open Blink, type ssh mac, and you're in. Your Claude Code session is waiting exactly where you left it.
Prerequisites
Before we start:
- macOS with Homebrew installed
- ngrok account (free tier works, paid recommended for reserved domains)
- Blink Shell on iPhone (or any SSH client)
- Claude Code CLI installed
# Install required tools
brew install ngrok tmux
# Authenticate ngrok (get token from dashboard.ngrok.com)
ngrok authtoken YOUR_AUTH_TOKEN
Step 1: Reserve Your Domains and Addresses
This is where you set up your stable endpoints. Without reserved addresses, ngrok gives you random URLs that change every restart - not great for muscle memory.
In the ngrok dashboard:
- Reserve a TCP address for SSH (e.g.,
tcp://5.tcp.ngrok.io:12345) -
Go to: Cloud Edge → TCP Addresses → Reserve
-
Reserve HTTP domains for your dev servers (optional but recommended)
- Go to: Cloud Edge → Domains
- Free tier:
yourname-dev.ngrok-free.app - Paid tier:
yourapp-dev.ngrok.appor custom domain
Write these down. You'll use them in the config.
Step 2: Create a Traffic Policy
This is the security layer that runs at ngrok's edge - before traffic ever reaches your Mac. It blocks the constant barrage of WordPress scanners, SQL injection attempts, and other garbage that hits any public endpoint.
mkdir -p ~/ngrok-policies
cat > ~/ngrok-policies/default-policy.yml << 'EOF'
# Block bots, scanners, and add security headers
on_http_request:
# Block WordPress/PHP scanners
- name: "Block Scanner Traffic"
expressions:
- "req.url.path.startsWith('/wp-')"
- "req.url.path.contains('wp-admin')"
- "req.url.path.contains('wp-login')"
- "req.url.path.contains('xmlrpc.php')"
- "req.url.path.startsWith('/phpmyadmin')"
- "req.url.path.startsWith('/pma')"
- "req.url.path.contains('.env')"
- "req.url.path.contains('.git')"
- "req.url.path.endsWith('.sql')"
- "req.url.path.endsWith('.bak')"
actions:
- type: "deny"
config:
status_code: 404
# Rate limit API endpoints
- name: "Rate Limit API"
expressions:
- "req.url.path.startsWith('/api/')"
actions:
- type: "rate-limit"
config:
name: "api_requests"
algorithm: "sliding_window"
capacity: 100
rate: "1m"
bucket_key:
- "req.headers['x-forwarded-for']"
# Block known scanner user-agents
- name: "Block Scanners"
expressions:
- "size(req.headers['user-agent']) > 0 && req.headers['user-agent'][0].contains('sqlmap')"
- "size(req.headers['user-agent']) > 0 && req.headers['user-agent'][0].contains('nikto')"
- "size(req.headers['user-agent']) > 0 && req.headers['user-agent'][0].contains('nmap')"
- "size(req.headers['user-agent']) == 0"
actions:
- type: "deny"
config:
status_code: 403
on_http_response:
- name: "Add Security Headers"
actions:
- type: "add-headers"
config:
headers:
X-Frame-Options: "DENY"
X-Content-Type-Options: "nosniff"
X-XSS-Protection: "1; mode=block"
Referrer-Policy: "strict-origin-when-cross-origin"
EOF
This policy:
- Returns 404 for WordPress/PHP scanner paths (they'll move on)
- Rate limits API endpoints to 100 requests/minute per IP
- Blocks requests with known scanner user-agents or no user-agent
- Adds security headers to all responses
Step 3: Configure ngrok
Now create the ngrok config file that defines your tunnels:
cat > "$HOME/Library/Application Support/ngrok/ngrok.yml" << 'EOF'
version: "3"
agent:
authtoken: YOUR_AUTH_TOKEN_HERE
endpoints:
# SSH access (required for remote development)
- name: mac-ssh
url: tcp://5.tcp.ngrok.io:YOUR_RESERVED_PORT
upstream:
url: localhost:22
# Dev server - view your app on your phone
# Works with any framework: Django, Flask, Node, Rails, etc.
# Just start your server on port 8000 and it's exposed
- name: dev
url: https://YOUR-RESERVED-DOMAIN.ngrok.app
upstream:
url: http://localhost:8000
traffic_policy_file: /Users/YOUR_USERNAME/ngrok-policies/default-policy.yml
EOF
Replace the placeholders:
- YOUR_AUTH_TOKEN_HERE - from ngrok dashboard
- YOUR_RESERVED_PORT - your reserved TCP port
- YOUR-RESERVED-DOMAIN - your reserved domain
- YOUR_USERNAME - your macOS username
Test it:
ngrok config check
If it says "valid", you're good.
Step 4: Auto-Start ngrok on Boot
We want ngrok running as soon as we log in. LaunchAgents handle this:
cat > ~/Library/LaunchAgents/com.user.ngrok-tunnels.plist << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.user.ngrok-tunnels</string>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/bin/ngrok</string>
<string>start</string>
<string>--all</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/Users/YOUR_USERNAME/Library/Logs/ngrok-tunnels.log</string>
<key>StandardErrorPath</key>
<string>/Users/YOUR_USERNAME/Library/Logs/ngrok-tunnels.log</string>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
<key>HOME</key>
<string>/Users/YOUR_USERNAME</string>
</dict>
</dict>
</plist>
EOF
# Replace YOUR_USERNAME with your actual username
sed -i '' "s/YOUR_USERNAME/$USER/g" ~/Library/LaunchAgents/com.user.ngrok-tunnels.plist
# Load it
launchctl load ~/Library/LaunchAgents/com.user.ngrok-tunnels.plist
The KeepAlive key means macOS will restart ngrok if it crashes. Nice.
Step 5: Create the tmux Session Script
tmux is what makes this whole thing work. Your session persists even when you disconnect - you can attach from your phone, do some work, detach, and come back later exactly where you left off.
mkdir -p ~/bin
cat > ~/bin/start-dev-session.sh << 'EOF'
#!/bin/bash
SESSION="dev"
WORKDIR="$HOME/projects" # Change to your project directory
# Kill existing session if it exists
tmux has-session -t $SESSION 2>/dev/null && tmux kill-session -t $SESSION
# Create new session with Claude Code
tmux new-session -d -s $SESSION -n "claude" -c $WORKDIR
tmux send-keys -t $SESSION:0 "claude" C-m
# Add a terminal window
tmux new-window -t $SESSION:1 -n "terminal" -c $WORKDIR
echo "Session '$SESSION' created. Attach with: tmux attach -t $SESSION"
EOF
chmod +x ~/bin/start-dev-session.sh
This creates a tmux session with:
- Window 0: Claude Code already running
- Window 1: A regular terminal for running servers, git commands, etc.
Step 6: Auto-Start the tmux Session
Same idea as ngrok - we want the dev session ready when we log in:
cat > ~/Library/LaunchAgents/com.user.dev-session.plist << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.user.dev-session</string>
<key>ProgramArguments</key>
<array>
<string>/Users/YOUR_USERNAME/bin/start-dev-session.sh</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>StandardOutPath</key>
<string>/Users/YOUR_USERNAME/Library/Logs/dev-session.log</string>
<key>StandardErrorPath</key>
<string>/Users/YOUR_USERNAME/Library/Logs/dev-session.log</string>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
<key>HOME</key>
<string>/Users/YOUR_USERNAME</string>
</dict>
</dict>
</plist>
EOF
# Replace YOUR_USERNAME
sed -i '' "s/YOUR_USERNAME/$USER/g" ~/Library/LaunchAgents/com.user.dev-session.plist
# Load it
launchctl load ~/Library/LaunchAgents/com.user.dev-session.plist
Step 7: The Watchdog
Things break. Networks hiccup. ngrok occasionally loses connection. The watchdog checks every 5 minutes and restarts things if needed.
cat > ~/bin/ngrok-watchdog.sh << 'EOF'
#!/bin/bash
# ngrok Tunnel Watchdog
# Checks if tunnels are healthy and restarts if needed
# Runs every 5 minutes via LaunchAgent
LOG_FILE="$HOME/Library/Logs/ngrok-watchdog.log"
LAUNCH_AGENT="$HOME/Library/LaunchAgents/com.user.ngrok-tunnels.plist"
API_TIMEOUT=5
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
}
# Trim log file if too large (keep last 1000 lines)
if [ -f "$LOG_FILE" ] && [ $(wc -l < "$LOG_FILE") -gt 2000 ]; then
tail -1000 "$LOG_FILE" > "$LOG_FILE.tmp" && mv "$LOG_FILE.tmp" "$LOG_FILE"
fi
log "=== Watchdog check ==="
# Check if ngrok API is responding
API_RESPONSE=$(curl -s --max-time $API_TIMEOUT http://localhost:4040/api/tunnels 2>/dev/null)
CURL_EXIT=$?
if [ $CURL_EXIT -ne 0 ] || [ -z "$API_RESPONSE" ]; then
log "ERROR: ngrok API not responding"
API_HEALTHY=false
else
# Check if we have tunnels
TUNNEL_COUNT=$(echo "$API_RESPONSE" | python3 -c "
import sys, json
try:
data = json.load(sys.stdin)
print(len(data.get('tunnels', [])))
except:
print(0)
" 2>/dev/null)
if [ "$TUNNEL_COUNT" -gt 0 ]; then
log "OK: $TUNNEL_COUNT tunnel(s) healthy"
API_HEALTHY=true
else
log "ERROR: No tunnels found"
API_HEALTHY=false
fi
fi
# If unhealthy, restart ngrok
if [ "$API_HEALTHY" != "true" ]; then
log "ACTION: Restarting ngrok tunnels..."
# Kill existing ngrok processes
pkill -f "ngrok start" 2>/dev/null
sleep 2
# Reload LaunchAgent
launchctl unload "$LAUNCH_AGENT" 2>/dev/null
sleep 2
launchctl load "$LAUNCH_AGENT" 2>/dev/null
# Wait and verify
sleep 10
VERIFY=$(curl -s --max-time $API_TIMEOUT http://localhost:4040/api/tunnels 2>/dev/null)
if [ -n "$VERIFY" ]; then
log "SUCCESS: Tunnels restarted"
else
log "ERROR: Tunnels still not responding after restart"
fi
fi
EOF
chmod +x ~/bin/ngrok-watchdog.sh
And the LaunchAgent to run it every 5 minutes:
cat > ~/Library/LaunchAgents/com.user.ngrok-watchdog.plist << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.user.ngrok-watchdog</string>
<key>ProgramArguments</key>
<array>
<string>/Users/YOUR_USERNAME/bin/ngrok-watchdog.sh</string>
</array>
<key>StartInterval</key>
<integer>300</integer>
<key>RunAtLoad</key>
<true/>
<key>StandardOutPath</key>
<string>/Users/YOUR_USERNAME/Library/Logs/ngrok-watchdog-stdout.log</string>
<key>StandardErrorPath</key>
<string>/Users/YOUR_USERNAME/Library/Logs/ngrok-watchdog-stderr.log</string>
</dict>
</plist>
EOF
# Replace YOUR_USERNAME
sed -i '' "s/YOUR_USERNAME/$USER/g" ~/Library/LaunchAgents/com.user.ngrok-watchdog.plist
# Load it
launchctl load ~/Library/LaunchAgents/com.user.ngrok-watchdog.plist
Step 8: Enable SSH on Your Mac
This is the one GUI step:
- System Settings → General → Sharing → Remote Login → Enable
- Set to allow access for your user
Step 9: Set Up SSH Keys
On your iPhone in Blink, generate an SSH key:
# In Blink terminal
ssh-keygen -t ed25519 -C "iphone-blink"
cat ~/.ssh/id_ed25519.pub
Copy that public key and add it to your Mac:
# On your Mac
echo "ssh-ed25519 AAAA... iphone-blink" >> ~/.ssh/authorized_keys
Keys are more secure than passwords and won't prompt you every time.
Step 10: Configure Blink
In the Blink app on your iPhone:
- Hosts → Add New Host
- Alias:
mac(or whatever you want) - Hostname:
5.tcp.ngrok.io(your ngrok TCP address) - Port:
YOUR_RESERVED_PORT - User: your macOS username
-
Key: select your SSH key
-
Save and test:
ssh mac
Using It
From your iPhone in Blink:
# Connect to your Mac
ssh mac
# Attach to the dev session
tmux attach -t dev
# You're now in Claude Code!
Detach without killing session: Ctrl+B then D
The full loop:
# 1. In Claude Code, build something
# 2. Open a new tmux window (Ctrl+B, c) and start your server:
python manage.py runserver 8000 # Django
npm run dev -- --port 8000 # Node/Vite
rails server -p 8000 # Rails
# 3. Open your phone browser to your ngrok domain
# https://YOUR-DOMAIN.ngrok.app
# 4. See your app, go back to Claude Code, iterate
This works with any framework. Django, Flask, FastAPI, Node, Rails, Go - if it runs on localhost, ngrok can expose it.
Verify Everything Works
Back on your Mac, run these checks:
# Check ngrok is running
curl -s http://localhost:4040/api/tunnels | jq -r '.tunnels[] | "\(.name): \(.public_url)"'
# Check tmux session exists
tmux list-sessions
# Check LaunchAgents are loaded
launchctl list | grep -E "ngrok|dev-session"
# Check watchdog logs
tail -20 ~/Library/Logs/ngrok-watchdog.log
Troubleshooting
Can't SSH through tunnel:
# Check tunnel is running
curl -s http://localhost:4040/api/tunnels | jq '.tunnels[] | select(.proto=="tcp")'
# Check SSH is enabled
nc -zv localhost 22
tmux session not starting:
# Check logs
tail -50 ~/Library/Logs/dev-session.log
# Manually start
~/bin/start-dev-session.sh
ngrok not starting on boot:
# Check logs
tail -50 ~/Library/Logs/ngrok-tunnels.log
# Reload LaunchAgent
launchctl unload ~/Library/LaunchAgents/com.user.ngrok-tunnels.plist
launchctl load ~/Library/LaunchAgents/com.user.ngrok-tunnels.plist
Security Notes
A few things to keep in mind:
- SSH uses key-only auth - Password auth works but keys are more secure
- Traffic policies block scanners - Check the ngrok inspector at http://localhost:4040 to see what's being blocked
- TCP tunnels can't use policies - The SSH tunnel is open to the internet; secure it with strong keys
- Reserved addresses are stable - Your SSH endpoint won't change between restarts
Limitations
This setup does not handle:
- System updates/restarts - You need physical or screen-sharing access to the Mac
- Power failures - LaunchAgents are user-level; your Mac account must be logged in for them to run. Enable auto-login or use a UPS.
The Result
I've been using this setup for months now. I've written features from airports, debugged production issues from coffee shops, and done code reviews from the couch. The experience is surprisingly good - Blink's keyboard support is excellent, and Claude Code's terminal interface works great on a phone screen.
Is it as fast as sitting at my desk? No. But it's development from anywhere, and that's worth the tradeoff.
Questions about the setup? Found a way to improve it? Reach out - I'd love to hear how others are approaching mobile development.