Manage Claude Code sessions from your phone -- or anywhere -- over a secure Tailscale VPN connection.
A Python package that runs a FastAPI server on your Mac, managing Claude Code CLI as subprocesses. A companion Expo app connects to this server over Tailscale, giving you full session management from your phone: create sessions, stream output live, approve tool use, and get push notifications.
Expo App (Phone) --> Tailscale VPN --> Mac --> FastAPI Server --> Claude Code CLI
|
Port 8080
REST + WebSocket API
| Tool | Cost |
|---|---|
| Tailscale | Free for personal use |
| FastAPI + Uvicorn | Free and open-source |
| Claude Code CLI | Any plan that includes Claude Code (Max, Pro, or API) |
You can also run Claude Code with local models via Ollama for a fully free setup.
- macOS (Apple Silicon or Intel)
- Python 3.10+
- Tailscale account + app installed on Mac and phone
- Claude Code CLI installed and authenticated
pip install -e ".[dev]"
ccr doctorInstall Tailscale on your Mac and phone, sign in with the same account:
tailscale ip -4 # Should print your Mac's Tailscale IP (100.x.y.z)ccr install # Registers PreToolUse hook in ~/.claude/settings.jsonccr start # Foreground mode, binds to Tailscale IP
ccr start -d # Daemon mode (background)
ccr start --no-auth # Local dev mode (127.0.0.1, no auth)The server starts on port 8080 by default. Open http://<tailscale-ip>:8080/api/status to verify.
Install the companion Expo app on your phone and point it at your server's Tailscale address.
| Command | Description |
|---|---|
ccr start |
Start the API server |
ccr start -d |
Start in background (daemon mode) |
ccr start --no-auth |
Start without Tailscale auth (local dev) |
ccr start --menubar |
Start with macOS menubar status indicator |
ccr stop |
Stop the API server (detects orphaned processes) |
ccr status |
Show server and Tailscale status |
ccr doctor |
Check all prerequisites |
ccr install |
Install the CCR approval hook into Claude Code |
ccr uninstall |
Remove the CCR approval hook |
The ccr stop and ccr start commands include orphan process detection -- if a previous server crashed without cleaning up, they will detect the process holding the port via lsof and offer to kill it (SIGTERM, then SIGKILL after 2s).
| Endpoint | Method | Description |
|---|---|---|
/api/status |
GET | Server health check |
/api/sessions |
GET, POST | List or create sessions |
/api/sessions/search |
GET | Full-text search across sessions |
/api/sessions/{id} |
GET, DELETE, PATCH | Get, delete, or update (rename) a session |
/api/sessions/{id}/export |
GET | Export session data |
/api/sessions/{id}/send |
POST | Send a prompt to a session |
/api/sessions/{id}/approve |
POST | Approve tool use |
/api/sessions/{id}/deny |
POST | Deny tool use |
/api/sessions/{id}/pause |
POST | Pause a session |
/api/sessions/{id}/resume |
POST | Resume a paused session |
/api/sessions/{id}/archive |
POST | Archive a session |
/api/sessions/{id}/unarchive |
POST | Unarchive a session |
/api/sessions/{id}/git/status |
GET | Git status for session project |
/api/sessions/{id}/git/diff |
GET | Git diff for session project |
/api/sessions/{id}/git/branches |
GET | Git branches for session project |
/api/sessions/{id}/git/log |
GET | Git log for session project |
/api/sessions/{id}/collaborators |
POST, DELETE | Add or remove collaborators |
/api/templates |
GET, POST | List or create templates |
/api/templates/{id} |
PUT, DELETE | Update or delete a template |
/api/projects |
GET, POST | Scan for or register projects |
/api/projects/create |
POST | Create blank project (git init) |
/api/projects/clone |
POST | Clone a git repo (background) |
/api/projects/git-check |
GET | Check git/SSH setup |
/api/push/register |
POST | Register Expo push token |
/api/push/settings |
GET, PUT | Manage push settings |
/api/approval-rules |
GET, POST | List or create auto-approval rules |
/api/approval-rules/{id} |
DELETE | Delete an approval rule |
/api/approval-rules/check |
GET | Check if tool is auto-approved |
/api/mcp/servers |
GET, POST, DELETE | List, add, or remove MCP servers |
/api/mcp/servers/{name}/health |
GET | MCP server health check |
/api/usage |
GET | Claude API usage data |
/api/usage/history |
GET | Usage history over time |
/api/skills |
GET | List available skills |
/api/workflows |
GET, POST | List or create workflows |
/api/workflows/{id} |
GET, DELETE | Get or delete a workflow |
/api/workflows/{id}/run |
POST | Run a workflow |
/api/workflows/{id}/steps |
POST | Add step to a workflow |
/api/sessions/{id}/upload |
POST | Upload file attachments to a session |
/api/sessions/{id}/hide |
POST | Hide native session (?permanent=true for permanent) |
/api/sessions/{id}/unhide |
POST | Unhide a hidden native session |
/api/cron-jobs |
GET, POST | List or create cron jobs |
/api/cron-jobs/{id} |
GET, PATCH, DELETE | Get, update, or delete a cron job |
/api/cron-jobs/{id}/toggle |
POST | Enable or disable a cron job |
/api/cron-jobs/{id}/trigger |
POST | Manually trigger a cron job |
/api/cron-jobs/{id}/history |
GET | Get cron job run history |
/api/dashboard/sessions |
GET | Unified CCR + native session list (paginated) |
/api/dashboard/sessions/{id} |
GET | Full session detail with paginated messages |
/api/dashboard/sessions/{id}/resume |
POST | Resume a native session as a new CCR session |
/api/dashboard/analytics |
GET | Dashboard summary stats |
/api/dashboard/cron-jobs |
GET | Cron jobs with recent run history |
/api/internal/approval-request |
POST | Hook: request tool approval |
/api/internal/statusline |
POST | Hook: receive statusline data |
/ws/sessions/{id} |
WS | Live session event stream |
/ws/terminal/{project_id} |
WS | Interactive PTY terminal |
A built-in web dashboard at /dashboard/ provides a unified view of all sessions -- both CCR-managed and native Claude Code sessions found in ~/.claude/.
- Session list with sorting, filtering, and search across both CCR and native sessions
- Session detail with a visual message timeline
- Analytics bar showing active sessions, 7-day costs, top model, and active cron jobs
- Cron job management with table view, create/edit forms, and run history
- Resume native sessions -- pick up a native Claude Code session as a new CCR session
The dashboard reads native Claude Code sessions from ~/.claude/projects/ and ~/.claude/sessions/, estimating costs using model-specific token pricing.
Access it at http://<tailscale-ip>:8080/dashboard/ or via the "Open Dashboard" button in the menubar.
Schedule recurring Claude Code sessions with cron expressions:
# Via API
curl -X POST http://<server>/api/cron-jobs \
-H "Content-Type: application/json" \
-d '{"name": "Daily report", "schedule": "0 9 * * *", "prompt": "Generate the daily status report for {{date}}", "project_directory": "~/Developer/my-project"}'Execution modes:
- SPAWN -- creates a new session for each run
- PERSISTENT -- reuses a single session across runs
Template variables available in prompts:
{{date}},{{time}},{{datetime}}-- current date/time{{project}}-- project directory name{{run_number}}-- incremental run counter{{branch}}-- current git branch
Jobs are persisted to ~/.local/state/claude-code-remote/cron/ and run history is logged to cron_history.jsonl. Concurrent runs of the same job are automatically skipped.
Upload files from the mobile app to a session's project directory:
curl -X POST http://<server>/api/sessions/{id}/upload \
-F "files=@screenshot.png"- Files are saved to
claude-uploads/in the project root - Filenames are sanitized (directory traversal, unsafe characters, hidden files)
claude-uploads/is automatically added to.gitignore- Uploads are rejected for completed sessions
The server merges native Claude Code terminal sessions alongside CCR-managed sessions, enabling seamless switching between the terminal and the mobile app.
- Unified session list -- native sessions from
~/.claude/projects/appear in/api/sessionsalongside CCR sessions, filtered to the last 7 days by default (configurable vianative_max_age_days) - Transparent adoption -- sending a message to a native session from the app automatically "adopts" it as a CCR session with
--resume, preserving full conversation history - Conflict prevention -- active native processes (detected via PID) block concurrent access from the app
- JSONL sync -- the native JSONL file is the source of truth for conversation history; messages are synced at the start of each turn and on WebSocket connect, so terminal activity always appears in the app
- Non-destructive hide -- native sessions can be hidden (archive) or permanently hidden (delete) without touching the JSONL files; hidden sessions appear in the archive view
- Server hostname badge -- native sessions show the server's hostname in the app for multi-machine awareness
The server uses Claude Code's native stream-json event format throughout -- no translation layer. Both CCR and native sessions produce identical event structures.
The server includes a PreToolUse hook that routes Claude Code's tool calls to your phone for approval:
ccr install # Register the hook in ~/.claude/settings.json
ccr uninstall # Remove the hook- Safe tools (Read, Glob, Grep, etc.) are auto-approved
- Dangerous tools (Write, Edit, Bash) are routed to the mobile app for approval/denial
- Skip permissions mode auto-approves everything (per-session toggle)
Each session tracks real-time metadata extracted from Claude Code's stream-json output:
- Model -- current model name (e.g.,
claude-opus-4-6) - Context % -- estimated context window usage from token counts
- Git branch -- current branch of the project directory
- Cost -- cumulative API cost in USD
When started with --menubar, the server shows a macOS menubar indicator:
● CCR-- server running, no attention needed◉ CCR-- session awaiting tool approval○ CCR-- server not responding
The menu displays the Tailscale MagicDNS hostname, a live session list with status badges, and quick links to the dashboard. Click the address to copy the server URL.
The server binds exclusively to the Tailscale interface IP and authenticates requests via tailscale whois. Nothing is exposed to the public internet or local network. Tailscale creates a peer-to-peer WireGuard encrypted tunnel between your devices.
Config file: ~/.config/claude-code-remote/config.json
{
"port": 8080,
"max_concurrent_sessions": 5,
"scan_directories": ["~/Developer"],
"native_max_age_days": 7
}If your SSH keys are managed by 1Password (or another agent that requires interactive approval), Claude Code sessions run via CCR will hang waiting for approval when git operations need SSH access. Set up a dedicated local key:
# Generate a passphrase-less key for unattended use
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_local -N "" -C "$(whoami)@ccr-local"
# Add to GitHub (requires gh CLI authenticated)
gh ssh-key add ~/.ssh/id_ed25519_local.pub --title "CCR local key"Then add a Host github.com block in ~/.ssh/config above the Host * catch-all:
Host github.com
IdentityFile ~/.ssh/id_ed25519_local
IdentityAgent none
IdentitiesOnly yes
Host *
IdentityAgent "~/Library/Group Containers/2BUA8C4S2C.com.1password/t/agent.sock"
This ensures GitHub SSH uses the local key while everything else continues through 1Password.
pip install -e ".[dev]"
python -m pytest tests/ -v