
Mac mini を herdr サーバーにして外出先から Claude Code を操作する
はじめに
こんにちは、クラスメソッド製造ビジネステクノロジー部の嶋田です。
自宅に常駐させている Mac mini を herdr のサーバーにして、そこで動く Claude Code へ iPhone と MacBook から接続する環境を作りました。
herdr はエージェントの実行に特化したターミナルマルチプレクサです。
サーバーが常駐してペインを保持するため、クライアントをデタッチしてもエージェントは動き続けます。
この性質を使えば、手元のマシンを閉じても作業が進む環境になります。
本記事では、Tailscale で 3 台をつないでから、MacBook で指示を出して iPhone で承認できるまでの手順を紹介します。
途中で踏んだ落とし穴も、切り分けの過程ごと残しました。
検証時のバージョンは Mac mini が macOS 26.5.1、herdr 0.7.3、mosh 1.4.0、moshi-hook 0.2.70、Claude Code 2.1.220 です。
構成
登場するマシンは 3 台です。
Mac mini がホストになり、MacBook と iPhone がクライアントとして同じセッションへ入ります。
ホストを Mac mini に置いたのは、常時電源が入っていて場所も動かないマシンだからです。
持ち歩く MacBook は、蓋を閉じればスリープし、接続先の経路も移動のたびに変わります。
接続を待ち受ける役は据え置きのマシンに任せるほうが素直です。
ネットワークには Tailscale を使います。
自宅の Mac mini は NAT の内側にいるため、外から到達させるには通常ならポート開放が必要です。
Tailscale は WireGuard のメッシュを張って NAT を越えるので、ルーターの設定を触らずに済みます。
SSH のポートをインターネットへ晒さない点でも、こちらのほうが安全です。
Moshi は mosh プロトコルを使うため UDP が通る必要があります。
Tailscale なら tailnet 内の UDP がそのまま流れるので、この点も都合がよいです。
Tailscale で 3 台をつなぐ
3 台すべてに Tailscale を入れて、同じアカウントでログインします。
Mac mini では合わせて、システム設定の共有からリモートログインを有効にし、エネルギー設定でディスプレイオフ時のスリープを無効にしておきます。
$ brew install mosh tailscale herdr moshi-hook
$ sudo tailscale up
$ tailscale ip -4
つながったかどうかは、クライアント側から tailscale ping で確認します。
$ tailscale ping -c 3 mac-mini
pong from mac-mini (100.x.y.z) via 192.168.x.y:41641 in 12ms
via に続くアドレスは、パケットが実際に通った経路です。
ここが相手の LAN 側アドレスなら直接 UDP が通っており、Tailscale の中継サーバー(DERP)を経由していません。
この表示が via DERP になっている場合は中継経由なので、遅延が乗ります。
SSH の設定
Tailscale が通ったので、次は MacBook から Mac mini へ公開鍵を置きます。
以降の手順ではホストを mac-mini という別名で呼びます。
接続先の情報は ~/.ssh/config にまとめておきます。
ssh はユーザー名を省略すると手元のユーザー名をそのまま使うため、接続先のユーザー名はここで明示しておきます。
Host mac-mini
HostName 100.x.y.z
User <remote-user>
公開鍵は ssh-copy-id で置きます。
$ ssh-copy-id -f -i ~/.ssh/id_ed25519.pub mac-mini
-f -i で公開鍵を明示しているのは、SSH 鍵を 1Password のエージェントで管理しているためです。
~/.ssh/id_ed25519 という秘密鍵ファイルの実体が存在しないので、指定を省くと ssh-copy-id が ID ファイルを開けずに失敗します。
鍵が入ると、以降はパスワードなしで入れます。
$ ssh mac-mini 'sw_vers -productVersion'
26.5.1
Mac mini 側の herdr を整える
Mac mini には dotfiles を配ってあるため、herdr の設定ファイルとプラグインの実体はすでに置かれていました。
それでもプラグインは動いていませんでした。
$ ssh mac-mini 'herdr plugin list'
No plugins installed.
herdr のプラグインは、置くだけでは有効になりません。
herdr plugin link で登録した情報は herdr サーバー側の状態として保持されるため、dotfiles には含まれません。
マシンごとに実行する必要があります。
$ ssh mac-mini 'herdr plugin link ~/.config/herdr/plugins/claude-on-worktree'
$ ssh mac-mini 'herdr plugin list'
1 plugin installed:
- <user>.claude-on-worktree (Claude on worktree) enabled [local:...]
このプラグインは、worktree が作られたときにその初期ペインで Claude Code を起動します。
herdr integration install claude によるエージェント状態のフックは、導入済みの状態でした。
MacBook から worktree を作って Claude Code に指示する
準備ができたので、MacBook から Mac mini の herdr を操作します。
herdr の CLI は Unix ソケット越しにサーバーへ指示を送る作りです。
そのため対話的な端末を必要とせず、ssh にコマンドを渡す形でそのまま使えます。
$ ssh mac-mini 'herdr worktree create --cwd ~/src/github.com/<user>/workspace --branch remote-demo --no-focus'
worktree が新しいワークスペースとして作られ、プラグインが発火しました。
$ ssh mac-mini 'herdr plugin log list --limit 5'
... "event":"worktree.created","exit_code":0,"status":"succeeded" ...
ワークスペースの一覧を見ると、エージェントの状態が付いています。
$ ssh mac-mini 'herdr workspace list'
w1 dotfiles agent: unknown panes: 2
w2 workspace agent: unknown panes: 1
w3 remote-demo agent: idle panes: 1
idle は、Claude Code が起動してフックから herdr へ状態を通知した結果です。
herdr のサイドバーに出る色付きのインジケーターと同じ情報を、CLI からも読めます。
指示は herdr pane send-text で送り、改行は send-keys で送ります。
$ ssh mac-mini 'herdr pane send-text w3:p1 "remote-demo-proof.md を作成してください。中身は 1 行だけです。"'
$ ssh mac-mini 'herdr pane send-keys w3:p1 enter'
ペインの見た目は herdr pane read で読めます。
20 秒ほど待つと状態が blocked になり、読んでみると承認ダイアログが出ていました。
send-keys で Enter を送って承認すると done になり、ファイルが実際に作られていました。
$ ssh mac-mini 'cat ~/.herdr/worktrees/workspace/remote-demo/remote-demo-proof.md'
MacBook から Tailscale 経由で herdr のペインに指示を送りました
MacBook から Tailscale と SSH を通り、herdr のソケットを経由して Mac mini の Claude Code へ指示が届きました。
ここで、承認の扱いが問題になることが見えてきます。
手元にいれば承認は数秒で済みますが、離れているあいだは画面を見ていない時間だけエージェントが止まったままになります。
承認をどこで受けるかが、この構成を日常的に使えるかどうかを決めます。
この点は後半の moshi-hook の節で扱います。
herdr --remote でアタッチする
CLI から操作できることは確認できたので、TUI でアタッチします。
$ herdr --remote mac-mini
error: nested herdr is disabled by default.
see configuration if you want to enable it.
私のターミナルは起動時に herdr をアタッチする設定にしています。
そのため herdr --remote は herdr のペインの中から実行することになり、入れ子として弾かれました。
これを許可する設定は既定の設定ファイルにあります。
[experimental]
# Allow launching herdr from inside a herdr-managed pane.
allow_nested = true
これを追記して herdr server reload-config を実行すると、Mac mini のセッションへアタッチできます。
ただし、この形ではプレフィックスキーが手前の herdr に取られます。
手元とリモートで同じ設定ファイルを使っていると、どちらのプレフィックスも ctrl+q になるため、外側のクライアントが先に受け取ってしまい、リモート側のペインやタブを操作できません。
--remote-keybindings という選択肢もありますが、これはアタッチ側がどちらの設定のキーバインドを使うかを選ぶだけです。
外側のクライアントがキーを受け取る順序は変わらないため、この衝突は解消しませんでした。
そこで、herdr を起動していないウィンドウを新しく開き、そこで直接アタッチする形にしました。
Alacritty は -e でターミナル側のシェル設定を上書きできるので、herdr の自動起動を回避できます。
#!/bin/sh
exec open -na Alacritty --args -e /bin/zsh -lc "herdr --remote '$HERDR_REMOTE_HOST'"
手前に herdr が居ないため、プレフィックスはすべてリモートへ届きます。
リモート専用のウィンドウが 1 枚立ち上がる形になり、手元のセッションとも混ざりません。
サイドバーには Mac mini 側のワークスペースが並び、worktree で作ったワークスペースには Claude Code が居ます。

iPhone の Moshi からつなぐ
iPhone 側は Moshi を使います。
Moshi は mosh プロトコルで接続するターミナルアプリで、回線の切り替えやスリープをまたいでセッションが生き残ります。
Moshi のホスト登録は、moshi-hook の側から確認できます。
$ ssh mac-mini 'moshi-hook host list'
host_9d7e9285...:FXOxOBTZ <remote-user>@mac-mini.local:22 SHA256:FXOxOBTZ... active
host_9d7e9285...:HLQWkiro <remote-user>@mac-mini.local:22 SHA256:HLQWkiro... active
ここで登録されている宛先が mac-mini.local になっている点に注意が必要です。
.local は同じ LAN の中でしか解決できないため、外出先からは届きません。
アプリ側のサーバー設定には、Tailscale のアドレスを入れておきます。
SSH の接続とエージェントのフックは、Moshi では別の仕組みとして扱われます。
moshi-hook status が unpaired でも、SSH で接続することはできます。
接続してシェルで herdr を実行すると、Mac mini のセッションへアタッチできました。
Mac mini 側でプロセスを見ると、Moshi が herdr を認識していることが分かります。
$ ssh mac-mini 'ps -axo args | grep [m]osh-server'
mosh-server new -s -c 256 -l LANG=C.UTF-8 -- sh -lc ... herdr --session 'default' workspace focus 'w3' >/dev/null 2>&1; exec herdr --session 'default'
アタッチする前に workspace focus でワークスペースを選んでいます。
moshi-hook context にも herdr を判定する実装が入っており、tmux 以外のマルチプレクサへの対応が進んでいます。
一方で、tmux を前提にしている部分も残っています。
SSH と mosh のアクセスを準備する moshi-hook host setup は、前提チェックで tmux を要求します。
host prerequisites failed:
- tmux was not found on PATH
tmux を撤去して herdr へ移行した環境では、--force で強制的に続行することができます。

iPhone へ承認を飛ばす
先送りにしていた承認の問題を、ここで扱います。
moshi-hook は Claude Code のフックから承認要求を受け取り、iPhone へ送ります。
ペアリングは、アプリの Agent Hooks で取得したトークンを渡して行います。
これを SSH 越しに実行すると失敗しました。
store pairing token: add generic password: macOS Keychain is locked or unavailable
(security: SecKeychainItemCreateFromContent (<default>): User interaction is not allowed.)
ペアリング情報は login キーチェーンへ保存されます。
SSH のセッションにはキーチェーンへ書き込む権限がありません。
読み取りは通るため、既存の項目を確認するだけなら SSH からでもできます。
トークンをファイルへ保存する --store file もありますが、ここでは herdr を経由しました。
Mac mini の herdr サーバーは GUI のログインセッションから起動して常駐しているため、そのペインで動くプロセスは GUI セッションの権限を引き継ぎます。
$ ssh mac-mini 'herdr pane split w2:p1 --direction down --no-focus'
$ ssh mac-mini 'herdr pane run w2:p2 "moshi-hook pair --token <token>"'
$ ssh mac-mini 'herdr pane read w2:p2 --source visible'
Paired as mac-mini.local (host_9d7e9285...)
status: paired
常駐している herdr は、リモートから GUI セッション権限のコマンドを実行する経路としても使えます。
なお、この状態でも ssh から moshi-hook status を見ると unpaired と表示されます。
SSH のセッションがキーチェーンを読めないためで、ペアリングの成否はペイン側で確認します。
続いてフックを入れ、デーモンを常駐させます。
$ moshi-hook install --target claude
claude -> installed
$ brew services start moshi-hook
$ moshi-hook probe
installed: true
running: true
gateway: true
version: 0.2.70
ここでもう 1 つ、dotfiles 利用者には見落としやすい変更が起きます。
moshi-hook install は ~/.claude/settings.json が dotfiles への symlink であっても、通常のファイルへ置き換えます。
dotfiles 側には差分が出ないため、リンクが外れたことに気付きにくいです。
マージ後の内容を dotfiles へ取り込み、symlink を張り直して管理下へ戻しました。
追加されたフックは 9 個で、すべて Homebrew の絶対パスから moshi-hook を呼びます。
/opt/homebrew は Apple Silicon の Homebrew で共通なので、dotfiles で共有しても解決できます。
既存の herdr のフックはマージされて残りました。
これで、指示と承認を別のマシンで受け持てるようになります。
指示は MacBook から送り、承認は iPhone で返します。
フックを読み込ませるために Claude Code を再起動してから、MacBook から指示を送りました。
状態が blocked になったあと iPhone の Moshi で承認すると、done に変わってファイルが作られました。
$ ssh mac-mini 'cat ~/.herdr/worktrees/workspace/remote-demo/iphone-approved.md'
iPhone から承認しました
MacBook で指示を出し、iPhone で承認し、Mac mini が実行するところまでつながりました。

Raycast から herdr を操作する
herdr の操作には、ふだん Raycast の Herdr 拡張を使っています。

この拡張は herdr のソケット API をそのまま Raycast のコマンドとして並べたものです。
Dashboard からワークスペース、タブ、ペイン、エージェントを一覧して切り替えたり、ペインの分割やズームを実行したりできます。
エージェントに対しては、起動、指示の送信、承認待ちのエージェントへの移動といったコマンドが用意されています。
メニューバーへ状態を出すコマンドもあるので、ターミナルを見ていなくても手が止まっているエージェントに気付けます。
Git のリポジトリを扱うコマンドもあり、ブランチ名を渡すだけで worktree のチェックアウトと、それを開くワークスペースの作成がまとまって行われます。
worktree の一覧や削除も同じ場所から扱えるため、ブランチごとに作業場所を分ける進め方が負担なく回ります。
私の場合はここに worktree 作成時のプラグインが乗るので、ブランチ名を入力すると作業場所と Claude Code が同時に用意される形になります。
この拡張も、手元の herdr サーバーではなく Mac mini のサーバーへ向けられます。
鍵になるのは、herdr の CLI が環境変数 HERDR_SOCKET_PATH で接続先のソケットを差し替えられることです。
SSH は Unix ドメインソケットを転送できるので、Mac mini 上のソケットを手元へ引いて、そこへ CLI を向ければ、手元の herdr コマンドがそのままリモートのサーバーを操作します。
$ ssh -fnNT -L /tmp/herdr-remote.sock:/Users/<remote-user>/.config/herdr/herdr.sock mac-mini
$ HERDR_SOCKET_PATH=/tmp/herdr-remote.sock herdr workspace list
w1 dotfiles agent: unknown
w2 workspace agent: unknown
w3 remote-demo agent: idle
拡張には Herdr Binary という設定項目があり、呼び出す実行ファイルを指定できます。
そこで、トンネルを張ってから本物の herdr へ渡すラッパーを用意して、それを指すようにしました。
#!/bin/sh
set -eu
env_file="${XDG_CONFIG_HOME:-$HOME/.config}/herdr/remote.env"
[ -f "$env_file" ] && . "$env_file"
sock="${HERDR_REMOTE_SOCK:-/tmp/herdr-remote.sock}"
herdr_bin="${HERDR_REMOTE_BIN:-/opt/homebrew/bin/herdr}"
if [ ! -S "$sock" ] || ! HERDR_SOCKET_PATH="$sock" "$herdr_bin" status server >/dev/null 2>&1; then
rm -f "$sock"
remote_home=$(ssh -o BatchMode=yes "$HERDR_REMOTE_HOST" 'printf %s "$HOME"')
ssh -o BatchMode=yes -fnNT -L "$sock:$remote_home/.config/herdr/herdr.sock" "$HERDR_REMOTE_HOST"
fi
HERDR_SOCKET_PATH="$sock" exec "$herdr_bin" "$@"
接続先はスクリプトに書かず、dotfiles の管理外に置いた ~/.config/herdr/remote.env から読みます。
リモート側のソケットの位置は $HOME を問い合わせて組み立てるため、ホームディレクトリの位置が手元と違っても設定は要りません。
ソケットが無いときや応答しないときはトンネルを張り直すので、常駐の LaunchAgent を用意しなくても最初の 1 回で復旧します。
この方式が素直に動くのは、拡張がローカルのファイルシステムを見ていないからです。
ワークスペースもペインもエージェントも、識別子はすべてサーバーが払い出したものです。
リポジトリを扱うコマンドも herdr worktree create --workspace <id> --branch <name> のように、対象をワークスペースの ID で指定します。
リポジトリの場所を解決するのはサーバー側なので、ソケットの向き先を変えるだけで操作対象がまるごと入れ替わります。
その結果、Dashboard に並ぶのは Mac mini のワークスペースになり、エージェントへの指示も Mac mini で動いている Claude Code へ届きます。
メニューバーの状態表示も Mac mini のエージェントを映すので、手元の画面に何も出していなくても、リモートで手が止まったことに気付けます。
使い分けで気をつける点が 2 つあります。
Herdr Binary は 1 つしか指定できないため、拡張全体がリモート専用に切り替わります。
また、ターミナルを開く系のコマンドは手元でターミナルを起動します。
リモートのセッションを開きたい場合は、Custom Terminal Launcher に前の節のアタッチ用スクリプトを指定しておきます。
画面サイズは最小のクライアントに揃う
iPhone からアタッチすると、ペインの高さが変わりました。
$ ssh mac-mini 'herdr pane get w3:p1'
... "scroll": {"viewport_rows": 15} ...
iPhone がアタッチする前は 94 行でしたが、アタッチ後は 15 行になりました。
herdr は 1 つのセッションを複数のクライアントで共有するため、表示領域は最も小さいクライアントに合わせられます。
tmux で同じセッションを複数の端末から開いたときと同じ挙動です。
Claude Code の画面は横幅を使うので、この縮小は読みやすさに影響します。
両方の端末で同時にアタッチするような状況はあまり考えられないかもしれませんが、MacBook で腰を据えて作業するあいだは iPhone をデタッチしておくほうが快適です。
構成を移して変わったこと
得たものは 2 つあります。
1 つは、判断を求められる場所を手元のマシンから切り離せたことです。
承認を iPhone で受けられるので、席を離れているあいだもエージェントが止まったままにはなりません。
もう 1 つは、開発環境の Single Source of Truth を Mac mini に構築できたことです。
リポジトリも worktree もエージェントのセッションも Mac mini の上にあり、MacBook と iPhone はそこを覗くためのクライアントになりました。
どの端末から入っても同じ状態が見えるため、手元のマシンを増やしても環境を作り直す必要がありません。
おわりに
自宅の Mac mini を herdr のサーバーにして、MacBook と iPhone から操作する構成を紹介しました。
worktree ごとに Claude Code が動き、MacBook からは TUI と CLI と Raycast で、iPhone からは Moshi で同じセッションを扱えます。
つまずいた箇所は、どれもマシンをまたいだことで初めて表に出たものでした。
herdr のプラグイン登録はマシンごとに必要になり、ターミナルが herdr を自動起動する構成ではアタッチ用のウィンドウを別に用意することになります。
1 台で完結している環境では気付けない部分です。
次は Hermes Agent の導入を考えています。
Nous Research が公開している自己ホスト型の自律エージェントで、CLI と TUI に加えて Telegram や Slack などのメッセージング経由でも同じインスタンスに届きます。
記憶やスキル、セッションの履歴を手元の SQLite に持つため、常駐させるほど状態が積み上がる設計になっています。
今回構築した構成に載せやすい理由が 2 つあります。
herdr のエージェント検出は Hermes を対象に含んでおり、状態表示の連携も用意されています。
$ ssh mac-mini 'herdr integration status'
claude: current (v7) (/Users/<remote-user>/.claude/hooks/herdr-agent-state.sh)
hermes: not installed (/Users/<remote-user>/.hermes/plugins/herdr-agent-state/__init__.py)
moshi-hook install --target の対象にも Hermes が入っているため、承認カードの経路も本記事の構成をそのまま使えます。
つまり、Claude Code のために整えたサイドバーの状態表示と iPhone への承認転送が、そのまま Hermes の活用にも効きます。
活用の基盤は既に出来上がっているので、次回以降の記事で実際に触っていきます。









