English Version
出门四个小时,AI 编程助手还在干活。家里的 Mac,手机远程操控。
搭建花了 20 分钟。记录一下踩过的坑。
问题#
Claude Code 跑在终端里。终端长在电脑上。人离开电脑,终端就断了。对于一个能自己写代码、跑测试、改文件的工具来说,必须坐在桌前才能用,这不太对。
我的场景很具体:要去外地待两天,但 Claude Code 的对话上下文不想丢。想在高铁上用 iPad 看看它干了什么,审批几个改动,也许再布置个新任务。
SSH 是显而易见的答案。但手机上裸连 SSH 有几个问题:WiFi 切蜂窝 IP 会变,过隧道连接会断,从外网找到家里 Mac 的 IP 意味着折腾 DDNS 或者路由器端口转发。
我只想要一个开箱即用的方案。
技术栈#
四个组件,总共花了不到 20 块钱:
Tailscale(免费)给每台设备分配一个固定 IP。底层是 WireGuard,装在 Mac 和手机上,登录同一个账号,两台设备就能互相看到。不用端口转发,不用 DDNS,不用动防火墙。Mac 会拿到一个类似 100.124.178.101 的地址,全球可达。
tmux(免费)让终端会话在你断开后继续活着。在 tmux 里启动 Claude Code,合上笔记本,飞到另一个城市,SSH 回来,敲 tmux attach,一切回到原样。AI 的完整对话上下文都在。
Mosh(免费)是 SSH 的移动版替代。用 UDP 而不是 TCP,切网络时能无感重连。从 WiFi 走到蜂窝,Mosh 默默接上,没有 “broken pipe”,没有丢失会话。手机在路上用,这个很关键。
Echo($2.99,iOS)是把这些串起来的客户端。基于 Ghostty 终端引擎的 SSH + Mosh 客户端,界面干净,iPad 支持外接键盘。小屏幕上做终端操作,体验出乎意料地好。
搭建步骤#
Mac 端:
# 安装 Tailscalebrew install --cask tailscale
# 安装 Moshbrew install mosh
# 开启 SSH 访问# 系统设置 → 通用 → 共享 → 远程登录 → 打开
# 防火墙放行 Moshsudo /usr/libexec/ApplicationFirewall/socketfilterfw --add /opt/homebrew/bin/mosh-serversudo /usr/libexec/ApplicationFirewall/socketfilterfw --unblockapp /opt/homebrew/bin/mosh-server手机或 iPad:App Store 装 Tailscale 和 Echo。Tailscale 登录同一个账号。Echo 里新建服务器,填 Mac 的 Tailscale IP、用户名,协议选 Mosh。
搞定。
日常用法#
每天的流程很简单:
- Mac 上在 tmux 里启动 Claude Code:
tmux new -s claude - 正常工作。要走的时候直接走,连 detach 都不用。
- 手机上打开 Echo,点服务器,敲
tmux attach。
你看到的就是同一个终端。Claude Code 的完整对话都在。可以看它做了什么,审批改动,或者给新指令。
用完了 Ctrl+B d detach。会话继续在 Mac 上活着。回到桌前,开终端,tmux attach,无缝衔接。
一个细节:如果 Mac 和手机同时 attach 同一个 tmux 会话,窗口会缩到最小屏幕的尺寸,右边全是点。解决办法很简单,另一端 detach 就行。
踩坑记录#
搭建过程中碰到三个问题,你大概率也会碰到。
macOS 远程登录默认关着。 SSH 连上去 “Connection refused”,得去系统设置里手动打开。藏在 通用 → 共享 里面,不容易发现这就是 SSH 服务的开关。
防火墙会把 Mosh 吞掉。 macOS 自带的应用防火墙会默默丢弃 Mosh 的 UDP 包,连个报错都不给。SSH 没事(TCP 22 端口随远程登录自动放行),但 Mosh 需要 UDP 60000-61000。上面的 socketfilterfw 命令能解决,但需要 sudo。
iOS 一次只能开一个 VPN。 如果手机上用了 Clash 之类的代理,就不能同时开 Tailscale,只能手动切换。Mac 上没这个问题,Tailscale 只路由 100.x.x.x 的流量,不影响 HTTP 代理。
为什么值得折腾#
AI 编程助手已经到了不需要一直盯着的阶段。Claude Code 能自己调研一个话题、写一份文档、整理代码库、跑测试。但交互方式还停留在”人坐在电脑前”的假设上。
这套方案打破了这个假设。早上出门前布置任务,通勤路上手机检查进度,咖啡馆里审批 PR,睡前启动一个调研任务,第二天早餐时看结果。
用到的工具都不新。SSH 存在了几十年,tmux 比大多数 JavaScript 框架都老。Tailscale 让组网变得透明,Echo 让手机能当终端用。
说实话,手机终端终究是个妥协。审批改动没问题,但在手机键盘上写一段详细的 prompt 很慢。超过一句话的指令,我还是会等回到电脑前再说。这套方案适合巡查和转向,不适合深度工作。
Bark 推送通知#
之前的方案有一个摩擦点:我得主动打开终端看 Claude Code 是不是在等我。轮询,不是响应。解决办法是推送通知。
Bark 是一个开源的 iOS 推送服务。装 App,拿到一个 URL,任何 HTTP 请求打到这个 URL 就会变成手机通知。
Claude Code 有 hooks 系统。Notification 事件在 agent 完成任务或需要用户输入时触发。我写了一个 shell 脚本捕获这个事件,转发到 Bark,但有个关键设计:只在 SSH 会话中才推送。坐在 Mac 前用本地终端?不推。从手机远程连入?推。
判断依据很简单:$SSH_CONNECTION 这个环境变量只在 SSH 会话中存在。脚本第一行就检查它,不存在就直接退出。同一份 hook 配置在哪都能用,不需要手动开关。
脚本 ~/.claude/hooks/notify-bark.sh:
#!/bin/bash# 只在 SSH 会话中推送[ -z "$SSH_CONNECTION" ] && exit 0
BARK_URL="https://api.day.app/YOUR_BARK_KEY"
payload=$(cat)hook_event=$(echo "$payload" | jq -r '.hook_event_name // empty')message=$(echo "$payload" | jq -r '.["last-assistant-message"] // .message // .summary // empty')cwd=$(echo "$payload" | jq -r '.cwd // empty')
title="Claude Code"if [ -n "$cwd" ]; then title="Claude Code - $(basename "$cwd")"fi[ -z "$message" ] && message="${hook_event:-Event}"body=$(echo "$message" | head -c 200)
encoded_title=$(python3 -c "import urllib.parse; print(urllib.parse.quote('''$title'''))")encoded_body=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.stdin.read()))" <<< "$body")
curl -s -o /dev/null "$BARK_URL/$encoded_title/$encoded_body?group=claude-code" &在 .claude/settings.json 里注册:
{ "hooks": { "Notification": [ { "matcher": "", "hooks": [ { "type": "command", "command": "~/.claude/hooks/notify-bark.sh", "timeout": 10 } ] } ] }}现在工作流真正变成了响应式:布置任务,走开,干完了手机会响。
图片来自
图片来自