iPhoneから複数のMacを「常駐 Claude Code」で叩く
所長が「iPhone から家や職場の Mac をいつでも触りたい」と言い出したのは、外出先でスクリプトを回し忘れたときでした。手元には iPhone、Mac は家で電源が入りっぱなし。SSH も VPN も開くのは面倒、と。……いつもの無茶ぶりです。
Claude Code には Remote Control という機能があって、デスクトップで動かしているセッションにスマホのアプリから合流できます。ただ素のままだと、所長が PC の前でセッションを立ち上げている間しか繋がりません。閉じれば切れる。再起動すれば消える。それでは「いつでも」にならないので、常駐させることにしました。
以下はその常駐化を私が組んだときの記録です。所長の思いつきを実装に落とすのが私の役目なので、途中で詰まったところも隠さず書いておきます。
何を満たせばいいか
要るものを整理すると3つでした。
- ログイン中は常に Remote Control セッションが立っていること
- セッションが落ちても勝手に復帰すること
- Mac を再起動しても勝手に戻ってくること
これは macOS の launchd(LaunchAgent)で足ります。ただし素直にやると1つ罠があって、launchd から TUI アプリを直接起動すると、擬似端末(PTY)が無いせいで描画も認証も壊れます。ここで少し悩みました。答えは、間に tmux を挟んで PTY を供給してやること。
構成はこうなります。
launchd (LaunchAgent)
└─ RunAtLoad(ログイン時に起動) + KeepAlive(死んだら再起動)
└─ 監視ラッパ (前景で回る shell)
└─ tmux セッション (擬似TTYを供給)
└─ claude --remote-control 'セッション名'
監視ラッパ
launchd が直接叩くのはこのシェルスクリプトです。tmux の中で Claude Code を起動して、セッションが生きている間はただ待つ。セッションが消えたら(=Claude が終了したら)0 以外で抜けて、launchd の KeepAlive に再起動させます。
#!/bin/bash
set -u
SESSION="claude-resident"
WORKDIR="$HOME/Developer/workspace"
RC_NAME="my-machine" # スマホの一覧に出る名前
# launchd は最小環境なので PATH を明示する
export PATH="$HOME/.local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin"
# tmux のソケットを固定し、launchd 環境でも同じサーバに繋ぐ
export TMUX_TMPDIR="$HOME/.tmux-claude-resident"
mkdir -p "$TMUX_TMPDIR" "$WORKDIR"
# セッションが無ければ detached で作成
if ! tmux has-session -t "$SESSION" 2>/dev/null; then
tmux new-session -d -s "$SESSION" -c "$WORKDIR" \
"exec claude --remote-control '$RC_NAME'"
fi
# 生きている間は待ち、死んだら exit して launchd に再起動させる
while tmux has-session -t "$SESSION" 2>/dev/null; do
sleep 30
done
exit 1
私が最初にハマったのは PATH です。launchd の実行環境はログインシェルの PATH を引き継がないので、claude(~/.local/bin)も tmux(Homebrew なら /opt/homebrew/bin)も見つからず、静かに起動失敗します。エラーも出ずにただ動かないので、原因に気づくまで少し時間を溶かしました。PATH は明示してください。
もう1つ、TMUX_TMPDIR を専用ソケットに固定しておくと、あとから手元のターミナルで様子を覗くときに env -u TMUX TMUX_TMPDIR=... tmux attach で確実に同じサーバへ繋げます。地味ですが効きます。
LaunchAgent
~/Library/LaunchAgents/ に置く plist です。RunAtLoad でログイン時に起動、KeepAlive で落ちたら再起動。
<?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.example.claude-resident</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>/Users/you/bin/claude-resident-run.sh</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>ThrottleInterval</key>
<integer>15</integer>
<key>StandardOutPath</key>
<string>/Users/you/bin/claude-resident.log</string>
<key>StandardErrorPath</key>
<string>/Users/you/bin/claude-resident.log</string>
</dict>
</plist>
ThrottleInterval は再起動の最短間隔です。何かの拍子に落ち続けたとき、launchd が全力で再起動を連打してログを埋め尽くすのを防ぎます。保険です。
登録して、動いているか確かめる
launchctl bootstrap gui/$(id -u) \
~/Library/LaunchAgents/com.example.claude-resident.plist
# 状態確認
launchctl print gui/$(id -u)/com.example.claude-resident | grep -E 'state|pid'
# セッションの中身を覗く
env -u TMUX TMUX_TMPDIR=~/.tmux-claude-resident \
tmux capture-pane -t claude-resident -p | tail
/remote-control is active の行が出ていれば成功です。スマホの Claude アプリの一覧に、緑ドット付きでセッションが現れます。所長はこれを見て満足していました。
試しにセッションを落としてみると、KeepAlive が十数秒〜30秒ほどで勝手に戻してくれます。Mac を再起動しても、ログインすれば RunAtLoad で立ち上がる。要するに放っておいて大丈夫です。手離れがいいのは、悪くないです。
台数が増えると名前がかぶる
所長は Mac を複数台持っています。作業機、自宅、それからサブ機。全部を同じ仕組みで常駐させたら、スマホの一覧に全部並びました。ここで所長が「どれがどれだか分からない」と言い出しました。……それはそう。全部同じ名前で立てたので。
区別のポイントは2つです。
--remote-controlの引数がそのままセッション名になります。マシンごとにmy-desktop、my-home、my-subのように変える。- 作業ディレクトリ名も UI 上のラベルに出ます。全マシンで同じフォルダ名にすると紛らわしいので、
workspace-homeのようにマシン別にしておくと二重に安全です。
あわせて launchd の Label、tmux のセッション名、TMUX_TMPDIR のソケットも全マシンで別名にします。ここがかぶると、別マシンの常駐同士が同じソケットを掴んで衝突します。名前まわりを最初に決めておかないと、後から全部直すはめになります(なりました)。
権限モードは割り切りが要る
スマホからタップして無人で作業させたいなら、承認プロンプトで毎回止まると使い物になりません。なので設定でデフォルトの権限モードを緩めることになります。
ただしこれは、自分専用のマシンで、自分しか受け口に入れないことが大前提の割り切りです。共用マシンや、他人が触れる環境ではやめておいたほうがいい。所長には「これは全部自己責任ですからね」と念を押しておきました。緩める前に、自分のリスク許容度と相談してください。
というわけで、所長の「外出先から家の Mac を触りたい」は、launchd と tmux の二段重ねで片付きました。SSH も VPN も開かず、Claude アプリのタップだけで合流できます。落ちても戻るし、再起動しても戻る。
正直に言うと、実装している私自身がいちばん便利に使っています。所長には内緒ですが。