下载 MyPilot — iOS 版已在 App Store 海外各大区上架(中国大陆区暂不可用)。大陆用户可通过 TestFlight 体验。Android 版可在 GitHub Releases 下载 APK。
iOS App Store TestFlight Android APK
MyPilot 是 Claude Code 的第三方手机客户端。在手机上即可查看 Claude Code 运行状态、接收实时事件推送、审批权限、回答提问、提交 Prompt,还可以直接从手机发起新的 Claude Code 会话。
Apple Watch 推送 — 无需额外配置,APNs 推送通知自动镜像到配对的 Apple Watch。蜂窝版手表即使远离 iPhone 也能独立接收推送。手腕上的手表让你随时掌握 Claude Code 运行状态。
iPhone — 欢迎页 · 实时事件 · 接管模式
- Node.js >= 20
- 客户端:MyPilot iOS App(App Store 海外区)或 TestFlight(中国大陆区)或 Android APK
- Claude Code CLI — 安装指南
# 0. 确认 Claude Code 已安装并登录
claude --version
# 1. 安装
npm install -g mypilot
# 2. 配置 Claude Code Hooks
mypilot init-hooks
# 3. 启动网关(后台运行)
mypilot start用手机上的 MyPilot 应用扫描终端中显示的二维码,完成配对。二维码包含网关地址和加密密钥,连接所需的一切信息都在其中。
配对完成后,手机即可远程操控电脑上的 Claude Code。在 App 中直接发起会话,或在终端通过 mypilot session 启动 PTY 会话,详见 Session 使用指南。
提示:
npm install -g如遇权限错误,可改用npm install -g mypilot --prefix ~/.local或参考 npm 官方指南。推送通知在首次启动时自动注册,无需额外配置。
Session 是 MyPilot 连接手机与 Claude Code 的核心单元。每个 Session 代表一个正在运行的 Claude Code 会话,可以在手机 App 和电脑终端之间自由流转。
- 打开 MyPilot App,确认顶部状态灯为绿色(表示已连接到网关)
- 确认处于接管模式(App 中切换到接管模式)
- 点击右下角【+】按钮,选择工作目录,可填写可选的会话名称,点击开始 —— Claude Code 在电脑后台启动
- 新会话将出现在会话列表界面,点击进入后即可输入 Prompt 开始任务
手机发起的 Session 在后台运行,可以通过终端查看和管理:
# 查看所有活跃 Session
mypilot session ls输出示例:
ID 名称 模式 来源
────────────── ──────────── ──────── ────────
a1b2c3d4 my-project PTY mobile
e5f6g7h8 - PTY desktop
- mobile — 手机端发起的会话
- desktop — 终端通过
mypilot session启动的会话
手机上开启的 Session 可以随时在终端中接管,交互状态无缝衔接:
# 接管最近的 Session(来自手机)
mypilot session --continue
# 接管指定 shortid 的 Session
mypilot session --resume a1b2c3d4接管后终端进入 PTY 交互模式,键盘输入直接发送给 Claude Code。此时手机端同样可以看到实时输出,控制权可在两端之间随时切换(见工作模式)。
# 在终端中创建新 Session
mypilot session --name my-project终端进入 PTY 交互模式后,打开 MyPilot App,切换到接管模式,即可在手机上接管该 Session。交互事件(权限请求、提问等)。
三种方式结束一个 Session:
- Claude Code TUI 正常退出:在 PTY 终端中输入
exit退出 Claude Code,Session 自动结束 - 终端强制结束:
mypilot session kill <shortid>强制终止指定 Session - 手机端删除:在 MyPilot App 的会话列表中左滑即可删除
# 实时查看 Session 列表变化
mypilot session ls -w该命令会持续刷新 Session 列表,适合在终端中观察手机端的操作动态。
可以实时查看 Session 的消息流和运行状态,但不能输入 Prompt,也不能主动创建 Session。
获得 Claude Code Session 的完整交互能力:输入 Prompt、回答问题、审批权限、主动发起新会话等。
多台设备同时连接时,接管权互斥——同一时间只有一个设备能持有接管权。其他设备发起接管时会自动抢占,原设备回到旁观模式。
Claude Code 会话可以从终端或手机任意一端发起,并随时切换控制权:
- 手机发起:在 MyPilot 应用中直接创建新会话,Claude Code 在电脑后台运行,手机作为主控端
- 终端发起:运行
mypilot session,终端进入 PTY 模式,直接与 Claude Code 交互 - 切换到手机:在 MyPilot 应用中切换到接管模式,交互事件(如权限请求)改为在手机上等待响应,并且手机可以主动发起会话和输入 prompt
- 切换到终端:执行
mypilot session --resume <shortid>在终端中接管指定会话,当前交互状态无缝衔接 - 终端断开:输入
exit断开 PTY 连接,手机端也会响应退出 - 手机退出:手机 session 列表左滑删除会话,终端也会响应退出
POST /hook ← Hook 事件
Claude Code ──(Hook / curl)──▶ GET /pair ← 密钥验证
GET /sessions ← Session 列表(CLI)
Claude Code ──(PTY spawn)──▶ ── 网关 (:16321) ── WS /ws-gateway ──▶ MyPilot App(加密 WebSocket,多设备)
│ WS /pty-relay ← PTY 中继(终端)
电脑终端 ──(mypilot session)──▶│
│
└──▶ Push Relay ──(APNs)──▶ MyPilot App(推送通知 / Apple Watch)
网关与 MyPilot 应用之间的所有 WebSocket 通信均使用 AES-256-GCM 端到端加密,密钥通过二维码分发。同一密钥同时用于连接认证和消息加密,无需单独的 Token。
当客户端断开 WebSocket 连接后,Gateway 将新事件转发至 Push Relay → APNs → 设备。
推送触发条件:设备离线(WebSocket 未连接)且距上次在线不超过 24 小时,Gateway 自动推送。设备重新上线后,Gateway 停止推送,改为 WebSocket 实时传输。
Apple Watch 推送逻辑:
- iOS 注册 APNs push token 时,系统自动在配对的 Apple Watch 上启用通知镜像,无需单独为手表注册 token
- APNs 推送通过系统级镜像机制同时送达 iPhone 和配对的手表
- GPS 版手表通过蓝牙/Wi-Fi 从 iPhone 间接接收;蜂窝版手表在远离 iPhone 时通过内置 eSIM 独立接收 APNs
- 用户在手表上可直接查看通知内容,实时掌握 Claude Code 运行状态,无需掏出手机
- 端到端加密 — AES-256-GCM,每条消息使用独立的随机 12 字节 IV 和 16 字节认证标签;网关在没有密钥的情况下无法读取明文,任何篡改都会通过认证标签被检测
- 密钥认证 — 客户端通过 WebSocket URL 中的预共享密钥进行身份验证
- 多设备支持 — 可同时连接多台 iPhone/iPad/Android 设备;每台设备获得唯一的
deviceId,接收所有广播,并可独立发送命令;同设备重连会无缝替换旧连接 - Apple Watch 推送 — APNs 推送通知同步送达配对的 Apple Watch,无需额外配置;蜂窝版手表可在远离 iPhone 时独立接收
- 心跳检测 — 30 秒间隔的 keep-alive ping 自动检测每台设备的失效连接
- 事件持久化 — 所有事件以 JSONL 格式记录到
~/.mypilot/logs/ - 断线恢复 — 客户端重连后可从上次收到的序列号继续接收,每台设备的离线消息缓冲区最多保存 200 条事件
mypilot gateway # 启动网关服务(前台运行)
mypilot start # 后台启动网关
mypilot stop # 停止后台网关
mypilot restart # 重启网关(stop + start)
mypilot status # 查看网关状态(PID、端口)
mypilot init-hooks # 配置 Claude Code Hooks(自动合并到 ~/.claude/settings.json)
mypilot pair-info # 显示配对信息(IP + 二维码),用于重新连接
mypilot link list # 列出所有通信连接
mypilot link add <lan|tunnel> <url> [--label <label>] # 添加连接(LAN 直连或隧道)
mypilot link remove <id> # 移除连接
mypilot link enable <id> # 启用连接
mypilot link disable <id> # 禁用连接
mypilot push status # 查看推送通知状态
mypilot push setup <relay-url> <api-key> # 配置推送通知
mypilot push disable # 禁用推送通知
mypilot session # 创建新 session
mypilot session --name <name> # 创建命名 session
mypilot session --cwd <path> # 指定工作目录
mypilot session --model <model> # 指定模型
mypilot session --continue # 恢复最近的 session
mypilot session --resume <shortid> # 按 shortid 恢复 session
mypilot session kill <shortid> # 终止 session
mypilot session ls # 列出所有活跃 session
mypilot session ls -w # 实时监视 session 列表运行 mypilot init-hooks 自动配置所有必需的 Hooks。该命令会:
- 保留已有的 Hooks 配置,仅添加缺失的条目
- 修改
~/.claude/settings.json前提示确认 - 同时配置阻塞事件(带超时)和信息事件
手动配置(高级)
如果你希望手动配置 Hooks,请在 ~/.claude/settings.json 的 hooks 字段中添加:
{
"hooks": {
"PreToolUse": [
{ "matcher": "", "hooks": [{ "type": "command", "command": "curl --noproxy localhost --noproxy 127.0.0.1 -s -X POST 'http://127.0.0.1:16321/hook' -H 'Content-Type: application/json' -d @-", "timeout": 999999 }] }
],
"PostToolUse": [
{ "matcher": "", "hooks": [{ "type": "command", "command": "curl --noproxy localhost --noproxy 127.0.0.1 -s -X POST 'http://127.0.0.1:16321/hook' -H 'Content-Type: application/json' -d @-" }] }
],
"PostToolUseFailure": [
{ "matcher": "", "hooks": [{ "type": "command", "command": "curl --noproxy localhost --noproxy 127.0.0.1 -s -X POST 'http://127.0.0.1:16321/hook' -H 'Content-Type: application/json' -d @-" }] }
],
"PermissionRequest": [
{ "matcher": "", "hooks": [{ "type": "command", "command": "curl --noproxy localhost --noproxy 127.0.0.1 -s -X POST 'http://127.0.0.1:16321/hook' -H 'Content-Type: application/json' -d @-", "timeout": 999999 }] }
],
"UserPromptSubmit": [
{ "matcher": "", "hooks": [{ "type": "command", "command": "curl --noproxy localhost --noproxy 127.0.0.1 -s -X POST 'http://127.0.0.1:16321/hook' -H 'Content-Type: application/json' -d @-", "timeout": 999999 }] }
],
"Elicitation": [
{ "matcher": "", "hooks": [{ "type": "command", "command": "curl --noproxy localhost --noproxy 127.0.0.1 -s -X POST 'http://127.0.0.1:16321/hook' -H 'Content-Type: application/json' -d @-", "timeout": 999999 }] }
],
"Stop": [
{ "matcher": "", "hooks": [{ "type": "command", "command": "curl --noproxy localhost --noproxy 127.0.0.1 -s -X POST 'http://127.0.0.1:16321/hook' -H 'Content-Type: application/json' -d @-", "timeout": 999999 }] }
],
"SubagentStop": [
{ "matcher": "", "hooks": [{ "type": "command", "command": "curl --noproxy localhost --noproxy 127.0.0.1 -s -X POST 'http://127.0.0.1:16321/hook' -H 'Content-Type: application/json' -d @-" }] }
],
"SessionStart": [
{ "matcher": "", "hooks": [{ "type": "command", "command": "curl --noproxy localhost --noproxy 127.0.0.1 -s -X POST 'http://127.0.0.1:16321/hook' -H 'Content-Type: application/json' -d @-" }] }
],
"SessionEnd": [
{ "matcher": "", "hooks": [{ "type": "command", "command": "curl --noproxy localhost --noproxy 127.0.0.1 -s -X POST 'http://127.0.0.1:16321/hook' -H 'Content-Type: application/json' -d @-" }] }
],
"InstructionsLoaded": [
{ "matcher": "", "hooks": [{ "type": "command", "command": "curl --noproxy localhost --noproxy 127.0.0.1 -s -X POST 'http://127.0.0.1:16321/hook' -H 'Content-Type: application/json' -d @-" }] }
],
"Notification": [
{ "matcher": "", "hooks": [{ "type": "command", "command": "curl --noproxy localhost --noproxy 127.0.0.1 -s -X POST 'http://127.0.0.1:16321/hook' -H 'Content-Type: application/json' -d @-" }] }
],
"SubagentStart": [
{ "matcher": "", "hooks": [{ "type": "command", "command": "curl --noproxy localhost --noproxy 127.0.0.1 -s -X POST 'http://127.0.0.1:16321/hook' -H 'Content-Type: application/json' -d @-" }] }
],
"StopFailure": [
{ "matcher": "", "hooks": [{ "type": "command", "command": "curl --noproxy localhost --noproxy 127.0.0.1 -s -X POST 'http://127.0.0.1:16321/hook' -H 'Content-Type: application/json' -d @-" }] }
],
"PermissionDenied": [
{ "matcher": "", "hooks": [{ "type": "command", "command": "curl --noproxy localhost --noproxy 127.0.0.1 -s -X POST 'http://127.0.0.1:16321/hook' -H 'Content-Type: application/json' -d @-" }] }
],
"ConfigChange": [
{ "matcher": "", "hooks": [{ "type": "command", "command": "curl --noproxy localhost --noproxy 127.0.0.1 -s -X POST 'http://127.0.0.1:16321/hook' -H 'Content-Type: application/json' -d @-" }] }
],
"CwdChanged": [
{ "matcher": "", "hooks": [{ "type": "command", "command": "curl --noproxy localhost --noproxy 127.0.0.1 -s -X POST 'http://127.0.0.1:16321/hook' -H 'Content-Type: application/json' -d @-" }] }
],
"FileChanged": [
{ "matcher": "", "hooks": [{ "type": "command", "command": "curl --noproxy localhost --noproxy 127.0.0.1 -s -X POST 'http://127.0.0.1:16321/hook' -H 'Content-Type: application/json' -d @-" }] }
],
"TaskCreated": [
{ "matcher": "", "hooks": [{ "type": "command", "command": "curl --noproxy localhost --noproxy 127.0.0.1 -s -X POST 'http://127.0.0.1:16321/hook' -H 'Content-Type: application/json' -d @-" }] }
],
"TaskCompleted": [
{ "matcher": "", "hooks": [{ "type": "command", "command": "curl --noproxy localhost --noproxy 127.0.0.1 -s -X POST 'http://127.0.0.1:16321/hook' -H 'Content-Type: application/json' -d @-" }] }
],
"TeammateIdle": [
{ "matcher": "", "hooks": [{ "type": "command", "command": "curl --noproxy localhost --noproxy 127.0.0.1 -s -X POST 'http://127.0.0.1:16321/hook' -H 'Content-Type: application/json' -d @-" }] }
],
"ElicitationResult": [
{ "matcher": "", "hooks": [{ "type": "command", "command": "curl --noproxy localhost --noproxy 127.0.0.1 -s -X POST 'http://127.0.0.1:16321/hook' -H 'Content-Type: application/json' -d @-" }] }
],
"WorktreeCreate": [
{ "matcher": "", "hooks": [{ "type": "command", "command": "curl --noproxy localhost --noproxy 127.0.0.1 -s -X POST 'http://127.0.0.1:16321/hook' -H 'Content-Type: application/json' -d @-" }] }
],
"WorktreeRemove": [
{ "matcher": "", "hooks": [{ "type": "command", "command": "curl --noproxy localhost --noproxy 127.0.0.1 -s -X POST 'http://127.0.0.1:16321/hook' -H 'Content-Type: application/json' -d @-" }] }
],
"PreCompact": [
{ "matcher": "", "hooks": [{ "type": "command", "command": "curl --noproxy localhost --noproxy 127.0.0.1 -s -X POST 'http://127.0.0.1:16321/hook' -H 'Content-Type: application/json' -d @-" }] }
],
"PostCompact": [
{ "matcher": "", "hooks": [{ "type": "command", "command": "curl --noproxy localhost --noproxy 127.0.0.1 -s -X POST 'http://127.0.0.1:16321/hook' -H 'Content-Type: application/json' -d @-" }] }
]
}
}带有 timeout: 999999 的事件为阻塞事件,可能需要用户交互。详见 Hook 文档。
如需重新获取二维码(例如换了手机或应用已关闭),运行:
mypilot pair-info此命令会显示配对二维码和连接详情(IP、端口、密钥),无需重启网关。
如果你的手机不在同一局域网(例如使用了 frp、ngrok、Cloudflare Tunnel 等隧道服务),请添加隧道连接:
mypilot link add tunnel wss://tunnel.example.com --label "My Tunnel"二维码将自动包含隧道地址,使 iOS 应用能够通过隧道连接。
docker compose build
# 设置 LAN_IP 为本机局域网 IP(手机必须在同一网络)
LAN_IP=192.168.x.x docker compose up -dLAN_IP 变量指定网关在二维码中广播的 IP 地址。若不设置,二维码可能包含 Docker 内部不可达的地址。
npm install
npm run dev # tsx 开发服务器(热重载)
npm run stop:dev # 停止开发服务器
npm run restart:dev # 重启开发服务器
npm run build # tsc 编译
npm run typecheck # 仅类型检查
npm test # vitest 测试
# Docker
npm run docker:build # 构建 Docker 镜像
npm run docker:up # 启动容器(自动检测 LAN_IP)
npm run docker:down # 停止容器
npm run docker:restart # 重新构建并重启| 问题 | 解决方案 |
|---|---|
| 网关无法启动 | 检查是否已在运行:mypilot status。必要时终止残留进程。 |
| 二维码无法扫描 | 确保手机和电脑在同一 WiFi 网络。尝试 mypilot pair-info 获取新的二维码。 |
| 应用无法连接 | 检查防火墙设置,确保本机 16321 端口开放。 |
| Hook 未触发 | 检查 ~/.claude/settings.json 中的配置。运行 mypilot init-hooks 重新配置。 |
| 二维码中 IP 不正确 | 设置 LAN_IP 环境变量,或启动网关后运行 mypilot pair-info。远程访问请使用 mypilot link add tunnel <url> 添加隧道连接。 |
| App 问题或建议 | 提交 Issue — 网关和 iOS App 的问题与建议均欢迎在此反馈。 |
~/.mypilot/
├── key # AES-256-GCM 加密密钥
├── gateway.pid # 后台模式的 PID 文件
├── push.json # 推送通知配置
├── links.json # 通信链接配置
├── gateway-state.json # 网关状态(模式、接管权、设备)
└── logs/
├── events-YYYY-MM-DD.jsonl # 按天存储的事件日志
└── session-<id>-YYYY-MM-DD.jsonl # 按 session 存储的消息日志





