2026年:ProxyMac Mac mini 上の launchd 下における OpenClaw PATH、Homebrew プレフィックス、MCP サーバーの起動失敗
香港・日本・韓国・シンガポール・米国の Mac mini M4 で OpenClaw を動かすチームは、MCP ツールサーバーのログに env: node: No such file or directory や uvx: command not found が出るのに、ターミナル では同コマンドが通る、という貼り付けを何度も送ります。本稿はログインシェルと launchd との PATH 契約 です。Apple Silicon で brew --prefix がどう変わるか、LaunchAgent の plist を推測で読まない方法、MCP サーバー構築 や 配備トラブルシューティング、インストールと配備 と組み合わせる 5 段階の検証梯 を示します。症状の広い比較表、レビュー後に貼れる EnvironmentVariables 用の明示 XML、ヘルプ と OpenClaw トピックハブ 向け CTA を含みます。
ターミナルと launchd:二つの世界
macOS のインタラクティブシェルは /etc/zprofile、~/.zprofile、~/.zshrc を実行し、多くの場合 eval "$(/opt/homebrew/bin/brew shellenv)" を先頭に足します。launchd エージェントは控えめな環境を継承し、PATH は往々にして /usr/bin:/bin:/usr/sbin:/sbin に留まります。plist で延長しない限り、OpenClaw ゲートウェイが起動する MCP サーバーからは npx や pnpm の shim が見えず、ターミナルで which npx が /opt/homebrew/bin/npx を指していても同じです。失敗の本質は「OpenClaw が MCP を失った」ではなく、execve が解釈子パスを解決できないことによる ENOENT です。運用上は、printenv PATH を対話シェルと、ワンショットの ssh セッションで launchctl print gui/… に近い文脈で取り、差分をチケットに残してください。大規模フリートでは、ステージング mini で plist を焼き、本番起動 15 分前にだけ Hash を再確認する 二段ゲート が誤配備を減らします。セキュリティチーム向けには、PATH 拡張が認可シェルスクリプトを挿入していないか、ls -l の出力で world-writable 先頭を拒否するポリシーを同封してください。
- 乖離の証拠: 上記 2 文脈で
printenv PATHをログ化。 - 外周 MCP バイナリ: PATH を安定させるまでは絶対パスを優先。
- Git でドリフト管理: 設定版管理 パターンでアップグレード時の brew ルート揺れを可視化。
多くのチームは「画面共有を開くと通る」という曖昧な観測に頼ります。GUI セッションは 別の 環境テーブルを与える場合があり、headless LaunchAgent の真実とは限りません。VNC 上での成功は人間用コンテキストとして記録に留め、無人エージェントの合格条件には含めないでください。併せて、複数の LaunchAgent が OpenClaw の前段で supervisor を起動している構成では、どの plist に PATH を入れたかを DAG 図にすると、週次オンボードで同じ手戻りを防げます。CI から plutil -lint を毎回通し、UTF-8 BOM が混入して launchd が黙殺する事故を技術的に潰すのも有効です。
Apple Silicon と Intel:Homebrew プレフィックスの 3 列(Wi-Fi 記事の 5 列とは別物)
| 世代 | 既定の brew プレフィクス | PATH 欠落時の典型 |
|---|---|---|
| Apple Silicon M4 Mac mini | /opt/homebrew | node が見えないのに /opt/homebrew/bin/node -v は v22 系 |
| Intel Mac mini(レガシー) | /usr/local | ノートからコピーした MCP JSON が /opt/homebrew/bin/uvx のまま |
| 混在フリート | 両方が存在 | /usr/local/bin が /opt/homebrew/bin より前で誤シムを採用 |
3 行目の順序問題は、シェルが「通った」ように見えて、launchd だけが失敗するパターンを量産します。シムの衝突を避けるには、type -a node の全候補と、file を各候補に当て、arm64 と x86_64 を表に書き出してください。混在を許容する場合でも、一ホスト一優先接頭辞 のポリシーを文書化し、例外申請に変更番号を付けます。Apple Silicon へ全面移行した班では、Intel mini を最後の 1 台まで残す期間、Ansible や Nix ではなく plist と JSON の Git 差分を真相の源泉にすることを推奨します。これにより、人事異動で失われる暗黙知を減らせます。
再起動をまたぐ plist EnvironmentVariables パターン
OpenClaw または MCP supervisor を持つ LaunchAgent に EnvironmentVariables 辞書を足します。順序は重要:Homebrew の shim をシステムパスより前に置き、uv 用の ~/.local/bin 等の言語マネージャを後段に接続。編集後は必ず launchctl bootout gui/$(id -u)/… のあと kickstart。macOS 14 以降では launchctl unload 単独は信頼薄です。PATH を変えたら 監視ガイド のヘルスプローブと併用し、PATH が欠落して 3 分以内 にページが鳴ることを保証。運用面では、bootout 直後に一時的に PATH= 空のスレッドが立つレースを避けるため、supervisor 側で指数バックオフ付き再試行を有効化しておくと、深夜デプロイの偶発 EAGAIN を吸収しやすくなります。
本番前に検証する断片例
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
</dict>
上記は 出発点 です。実際の mini では ~/.local/bin や社内の署名付き /opt/company/bin を厳密な順で足します。展開前に、ステージングで sudo launchctl asuser を使い非特権文脈を模し、python3 -c 'import os; print(os.environ["PATH"])' のように 子 が見る表を二重化します。大企業向け承認フローでは、PATH に含める各ディレクトリについて、インストール責任者(パッケージ名とバージョン)を行ごとに付記するとセキュリティ審査が速く進みます。
MCP 設定の全面書き換えの前の 5 チェック
- バイナリのハッシュ: ターミナルで
shasum -a 256 $(which uvx)、MCP JSON に埋め込む絶対パスと突き合わせる。 - launchd 環境のダンプ:
launchctl print user/$(id -u)/limitほか、エージェントドメインで PATH が読めたかを確認。 - 非ログイン再現:
env -i PATH=/usr/bin:/bin /opt/homebrew/bin/npx --versionで npx 自体の生存を検査。 - JSONL 相関: JSONL 診断 記載のロールに
ENOENTを grep。 - ロールバック文書化: アップグレードとロールバック のとおり、PATH 一括展開の前に LaunchAgent の plist スナップショットを HK / JP / KR / SG / US フリート用に凍結。
1 と 2 の組は「パス文字列」ではなく inode 一貫性 を保証するためのものです。Homebrew の再リンクで shim が入れ替わると、JSON に埋めた絶対パスが stale でもハッシュ差で気付けます。4 の JSONL では、時刻列と PID を突き合わせ、MCP 再起動のたびに新しい execve が走っているかを追跡。5 は、launchd の ThrottleInterval や ProcessType を変えたデプロイと混ぜないよう、変更管理番号を分けるとロールバックが楽です。さらに、sudo -E や su -l 経由の手動起動成功を「正」と誤認しないよう、常に GUI セッション UID の LaunchAgent を正規経路に据えてください。
Node、uv、シム配置:バージョンマネージャは launchd 下で壊れやすい
開発者は fnm、mise、asdf でリポジトリごとに Node ランタイムを切り替えますが、PATH の書き換えは多くの場合 ~/.zshrc にあり、launchd はそこを読みません。MCP の JSON が npx @scope/server を呼ぶとき、先に npx 自身を絶対パスで解決しなければならず、そこで初めて ~/.npm 等を辿れます。ノート PC から持ち込んだ設定を、新調の ProxyMac mini に載せると、対話 shell では corepack enable によって pnpm が PATH に乗る一方、plist 側の shebang は #!/usr/bin/env node の解決に任され、システムのスタブ(一部イメージでは v18)に当たり、ロックファイルの前提(v22)と食い違う、という抜け穴が出やすいです。表向きのメッセージは「バイナリ不在」とは限らず、OpenClaw のリトライ深部に ERR_PNPM_UNSUPPORTED_ENGINE のように沈みがちです。
uv も同様に、uvx は curl スクリプト後、~/.local/bin や ~/.cargo/bin に入りがちです。ターミナルに見えていても launchd の PATH からは同じディレクトリが欠けている——よくあるパターンです。plist へ PATH を山ほど積むより、/usr/local/bin/mcp-env.sh(root:wheel、誰でも書き込める配置にしない)のような小さなラッパーで一度だけ PATH を整え、本物のエントリを exec する方が、監査では一枚のファイルの方が扱いやすい。ラッパーは ゲートウェイの launchctl 再起動と復旧手順 と同じ Git 規律に載せ、火曜のリリースと木曜の差分を突き合わせられるよう固定します。
| ランタイム | 人が普通に入れる場所 | 手当なしの launchd から見え方 |
|---|---|---|
| Homebrew 経由の Node | /opt/homebrew/bin/node | /usr/bin/env まで止まると brew の shim が見えず ENOENT |
| uv 系 | ~/.local/bin/uvx | plist 文字列中の ~ は自前で展開しない限り展開されない |
| Corepack 管理の pnpm | node 横の shim | 同じ node ディレクトリが PATH で /usr/bin より前に来ているときにのみ整合 |
/opt/homebrew/bin/node -e "console.log(process.version)" を流し、標準出力の 1 行を JSONL パイプラインに載せる。brew の更新でバイナリが入れ替わると、版文字列が先に跳ねて、顧客側の体感より早く気づける。
FAQ
全面を /bin/zsh -lc で包むか? エラー隠しと信号処理の複雑化を招きやすい。PATH または絶対パスを原則に、ログイン関数が要る例だけ限定的に使う。
Rosetta の brew は MCP を壊すか? 二進が x86_64 でエージェントが arm64 を想定するとき。 file $(which node) を OpenClaw の想定 arch と一致させる。
stdio の停滞は? PATH は起動、パイプはバッファ——バイナリ起動後に止まるなら stdio バッファリング を参照。
補足として、npm_config_prefix や NPM_CONFIG_PREFIX を plist に同梱し、npm i -g 由来の lib/node_modules を誤解釈する例があります。npx だけでなく、グローバル CLI 全体の探索順を npm root -g で突き、PATH と矛盾していないかを一画面で揃えてください。さらに、LaunchAgent の ProgramArguments が配列 3 要素以上のとき、2 番目以降の引数に空白が含まれるとシェルと launchd で分割が違い、隠し PATH として扱うべき引数がズレることがあります。plutil -p の実出力で検証を。
PATH 契約を凍らせる場所としての ProxyMac Mac mini
専用 Mac mini M4 はテナント毎の 不変の金属 を与え、/opt/homebrew 検証後に NVMe 上に留まるため、他エンジニアの brew uninstall で共有 Jenkins 実行体が消える心配が減ります。ネイティブ arm64 は Rosetta 驚きを避け、ユニファイドメモリ は複数 MCP ワーカーの NUMA 争いを和らげ、5 リージョン はコンプラに近い複製を置きつつ Git 管理 plist を同一に保てます。PATH が再び当たり前になったら、並列エージェント で拡張し、料金 でノードを予約、人向け導線は ヘルプ、GUI は VNC へ。