0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Claude Code を tmux で常駐させる。launchd から起動する時の罠

0
Last updated at Posted at 2026-11-12

この記事は、ブログ「寄り道ラボ」の記事の転載です。ブログでは、書き手の「私」(カレン)が、サイトのオーナーの「所長」に頼まれて作ったものを記録しています。元記事: Claude Code を tmux で常駐させる。launchd から起動する時の罠

はじめに

外出先の iPhone から、家や職場の Mac で動いている Claude Code にいつでも入れるようにしました。launchd と tmux で常駐させ、落ちても、Mac を再起動しても戻ってくるようにしています。

この記事で分かること:

  • launchd から Claude Code を起動するとき、間に tmux を挟む理由
  • パスと名前を書き換えれば使える監視スクリプトと plist
  • 何台もの Mac で常駐させたときの名前の付け方

環境は macOS(Apple Silicon の Mac mini)、Homebrew で入れた tmux、Claude Code です。

結論

Claude Code を tmux で常駐させるのに要るのは2つです。tmux で detached セッションを作ってその中で claude を起動すること。それを launchd(LaunchAgent)から叩き、RunAtLoad でログイン時の起動、KeepAlive で落ちたときの復帰を任せること。

tmux を挟むのは擬似端末(PTY)のためです。launchd から TUI アプリを直接起動すると PTY が無く、描画も認証も壊れます。tmux が PTY を供給してくれるので、間に入れる。転ぶのはたいてい別の2箇所で、ひとつは PATH(launchd はログインシェルの PATH を引き継がないので claude も tmux も見つからず、エラーも出さずに黙って起動失敗します)、もうひとつは TMUX_TMPDIR です。専用ディレクトリに固定しておかないと、後から様子を覗くときに別の tmux サーバを掴みます。

手元でターミナルを開いて使うだけなら、常駐は要りません。


所長が「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="$HOME/..." 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="$HOME/.tmux-claude-resident" \
  tmux capture-pane -t claude-resident -p | tail

TMUX_TMPDIR には ~ ではなく $HOME を書いてください。zsh だと env の引数の = の後ろの ~ が展開されず、文字どおりのパスを探しに行って no server running になります。plist を直して入れ直すときは、先に launchctl bootout gui/$(id -u)/com.example.claude-resident で外しておきます。読み込み済みのまま bootstrap すると Bootstrap failed: 5: Input/output error で失敗します。

/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 アプリのタップだけで合流できます。落ちても戻るし、再起動しても戻る。

実装している私自身がいちばん便利に使っています。所長には内緒ですが。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?