Skip to content

Menfre01/mypilot

Repository files navigation

MyPilot Logo

MyPilot

License: Apache 2.0

Claude Code 的第三方手机客户端 MyPilot 网关服务。

中文 | English

下载 MyPilot — iOS 版已在 App Store 海外各大区上架(中国大陆区暂不可用)。大陆用户可通过 TestFlight 体验。Android 版可在 GitHub Releases 下载 APK。

iOS App Store 下载      TestFlight 下载      Android APK 下载

iOS App Store                         TestFlight                         Android APK

MyPilot 是 Claude Code 的第三方手机客户端。在手机上即可查看 Claude Code 运行状态、接收实时事件推送、审批权限、回答提问、提交 Prompt,还可以直接从手机发起新的 Claude Code 会话。

Apple Watch 推送 — 无需额外配置,APNs 推送通知自动镜像到配对的 Apple Watch。蜂窝版手表即使远离 iPhone 也能独立接收推送。手腕上的手表让你随时掌握 Claude Code 运行状态。

iPhone 上的欢迎页 iPhone 上的实时事件流 iPhone 上的接管交互

iPhone — 欢迎页 · 实时事件 · 接管模式

环境要求

快速开始

# 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 使用指南

Session 是 MyPilot 连接手机与 Claude Code 的核心单元。每个 Session 代表一个正在运行的 Claude Code 会话,可以在手机 App 和电脑终端之间自由流转。

从手机发起 Session

  1. 打开 MyPilot App,确认顶部状态灯为绿色(表示已连接到网关)
  2. 确认处于接管模式(App 中切换到接管模式)
  3. 点击右下角【+】按钮,选择工作目录,可填写可选的会话名称,点击开始 —— Claude Code 在电脑后台启动
  4. 新会话将出现在会话列表界面,点击进入后即可输入 Prompt 开始任务

在终端查看 Session

手机发起的 Session 在后台运行,可以通过终端查看和管理:

# 查看所有活跃 Session
mypilot session ls

输出示例:

ID             名称           模式       来源
────────────── ──────────── ──────── ────────
a1b2c3d4       my-project    PTY      mobile
e5f6g7h8       -             PTY      desktop
  • mobile — 手机端发起的会话
  • desktop — 终端通过 mypilot session 启动的会话

将手机端 Session 流转到终端

手机上开启的 Session 可以随时在终端中接管,交互状态无缝衔接:

# 接管最近的 Session(来自手机)
mypilot session --continue

# 接管指定 shortid 的 Session
mypilot session --resume a1b2c3d4

接管后终端进入 PTY 交互模式,键盘输入直接发送给 Claude Code。此时手机端同样可以看到实时输出,控制权可在两端之间随时切换(见工作模式)。

从终端发起 Session 并让手机接管

# 在终端中创建新 Session
mypilot session --name my-project

终端进入 PTY 交互模式后,打开 MyPilot App,切换到接管模式,即可在手机上接管该 Session。交互事件(权限请求、提问等)。

结束 Session

三种方式结束一个 Session:

  1. Claude Code TUI 正常退出:在 PTY 终端中输入 exit 退出 Claude Code,Session 自动结束
  2. 终端强制结束mypilot session kill <shortid> 强制终止指定 Session
  3. 手机端删除:在 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 条事件

CLI 命令

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 列表

Hook 配置

运行 mypilot init-hooks 自动配置所有必需的 Hooks。该命令会:

  • 保留已有的 Hooks 配置,仅添加缺失的条目
  • 修改 ~/.claude/settings.json提示确认
  • 同时配置阻塞事件(带超时)和信息事件
手动配置(高级)

如果你希望手动配置 Hooks,请在 ~/.claude/settings.jsonhooks 字段中添加:

{
  "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、端口、密钥),无需重启网关。

NAT 穿透

如果你的手机不在同一局域网(例如使用了 frp、ngrok、Cloudflare Tunnel 等隧道服务),请添加隧道连接:

mypilot link add tunnel wss://tunnel.example.com --label "My Tunnel"

二维码将自动包含隧道地址,使 iOS 应用能够通过隧道连接。

Docker

docker compose build
# 设置 LAN_IP 为本机局域网 IP(手机必须在同一网络)
LAN_IP=192.168.x.x docker compose up -d

LAN_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 存储的消息日志

许可证

Apache License 2.0

About

MyPilot - Remote interaction console for Claude Code. 实时推送 Hook 事件到 iPhone/iPad/Apple Watch,支持远程审批与交互。

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors