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 계약입니다. brew --prefix, plist 읽는 법, MCP 서버 구성, 배포 문제 해결, 설치·배포에 맞춘 5단 검증, 비교표, EnvironmentVariables XML, 도움말·OpenClaw 허브 링크를 담습니다.
터미널과 launchd: 두 세계
macOS 인터랙티브 셸은 /etc/zprofile·~/.zprofile·~/.zshrc를 실행하며, 보통 eval "$(/opt/homebrew/bin/brew shellenv)"로 PATH를 늘립니다. launchd는 최소 환경을 물려받아 PATH가 /usr/bin:/bin:/usr/sbin:/sbin에 머뭅니다. plist로 늘리지 않으면 OpenClaw가 띄우는 MCP에서 npx·pnpm shim이 보이지 않고, which npx가 /opt/homebrew/bin/npx여도 execve는 ENOENT입니다. printenv PATH를 셸과 launchctl print gui/… 맥락에 각각 남기세요. 대규모에선 스테이징에 plist를 굽고 본가동 15분 전 해시로 이중 확인하고, ls -l로 world-writable이 PATH 앞에 오지 않게 하세요.
- 증거: 두 맥락에서
printenv PATH를 로그. - 바깥 바이너리: PATH가 안정될 때까지 절대 경로.
- 드리프트: 구성 버전 관리로 brew 루트 흔들림을 기록.
“화면 공유를 열면 된다”는 희망은 GUI가 다른 env를 줄 수 있어 headless launchd의 기준이 못 됩니다. VNC 성공은 사람용으로만 남기고, 여러 LaunchAgent가 앞단에 있으면 어느 plist에 PATH를 넣었는지 DAG로 그리세요. CI에 plutil -lint를 항상 걸어 BOM으로 launchd가 죽는 것을 막으세요.
Apple Silicon·Intel: Homebrew 프리픽스 3열
| 세대 | 기본 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 앞이면 잘못된 shim |
3행의 순서 문제는 셸에선 “됨”인데 launchd만 실패하는 케이스를 양산합니다. type -a node·file로 arm64·x86_64를 표로 적으세요. 혼재를 허용해도 호스트당 한 우선 접두 정책을 문서로 적어 변경 번호를 답니다. plist와 JSON의 Git을 진실로 삼으면 팀 이탈 후에도 덜 깨집니다.
재부팅을 넘기는 plist EnvironmentVariables
OpenClaw·MCP supervisor LaunchAgent에 EnvironmentVariables 딕셔너리를 넣으세요. Homebrew shim을 시스템 앞에, uv는 ~/.local/bin 등을 뒤에. launchctl bootout 후 kickstart. macOS 14+에선 unload만 믿지 마세요. 모니터링으로 PATH 누락이 3분 안에 알리게 하고, bootout 직후 빈 PATH 레이스는 supervisor에 지수 백오프로 흡수하세요.
검증용 조각(운영 투입 전)
<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로 비권한 맥락을 흉내 내고, 자식 env를 한 번 더 확인하세요(예: python3 -c 'import os; print(os.environ.get("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에서
ENOENTgrep. - 롤백: 업그레이드대로 PATH 일괄 전에 HK / JP / KR / SG / US용 plist를 동결.
1·2는 문자열이 아니라 inode 일치를 봅니다. brew relink로 shim이 바뀌면 JSON 절대 경로가 stale해도 해시로 드러납니다. 4는 타임스탬프·PID로 execve를 추적. 5는 ThrottleInterval·ProcessType 배포와 변경 번호를 섞지 마세요. sudo -E 수동 성공을 정답으로 두지 말고 GUI UID LaunchAgent를 기준에 두세요.
Node·uv·심: 버전 관리자가 launchd에서 왜 터지나
많은 팀이 fnm·mise·asdf로 저장소마다 Node 런타임을 갈아끼는데, 이 도구들은 대개 ~/.zshrc에서만 PATH를 손봅니다. launchd는 그걸 읽지 않습니다. MCP JSON이 npx @scope/server를 부르면 npx 자체가 먼저 절대 경로로 잡혀야 하고, 그다음에야 ~/.npm 쪽을 봅니다. 노트북에서 복사한 JSON을 새로 받은 ProxyMac mini에 붙이면, 대화형 셸에선 corepack enable로 pnpm이 PATH에 올라갔는데 plist 쪽 #!/usr/bin/env node는 시스템 껍데기(일부 이미지는 v18)에 걸리고, 락파일이 가정한 v22와 엇갈리는 사례가 흔합니다. 증상은 한 줄짜리 «바이너리 없음»이 아니라 OpenClaw 재시도 뒤에 묻힌 ERR_PNPM_UNSUPPORTED_ENGINE에 가깝습니다.
uv도 같습니다. uvx는 curl 설치 뒤 ~/.local/bin·~/.cargo/bin에 자주 놓이는데, 터미널에 보인다고 launchd PATH에도 같은 디렉터리가 있지는 않습니다. plist에 항목을 끝없이 쌓기보다, /usr/local/bin/mcp-env.sh 같은 얇은 래퍼를 root:wheel로 두고(전역 쓰기 권한 폴더는 앞에 두지 않음) 한곳에서 PATH를 내보내고 exec로 본 엔트리에 넘기는 편이 감사·리뷰엔 낫습니다. 래퍼는 게이트웨이 launchctl 재시작·복구 스니펫과 똑같이 Git으로 묶어 화요 배포와 목요 배포 사이 diff를 잡으세요.
| 런타임 | 사람이 흔히 설치한 위치 | 손 안 댄 launchd가 보는 것 |
|---|---|---|
| Homebrew Node | /opt/homebrew/bin/node | /usr/bin/env만 보이고 brew 심 누락 → ENOENT |
| uv 계열 | ~/.local/bin/uvx | plist 문자열의 물결은 직접 풀지 않으면 펼쳐지지 않음 |
| Corepack pnpm | node 옆 심 | 같은 node 디렉터리가 PATH에서 /usr/bin보다 앞에 올 때만 맞음 |
/opt/homebrew/bin/node -e "console.log(process.version)"을 실행하고, 표준출력 한 줄을 JSONL 파이프에 태우세요. brew 업그레이드로 바이너리가 갈리면 버전 문자열이 먼저 바뀌어 고객이 느끼기 전에 알 수 있습니다.
FAQ
전부 /bin/zsh -lc로 감쌀까? 오류 은닉·시그널이 복잡해집니다. 절대 경로·PATH를 원칙으로, 꼭 필요할 때만.
Rosetta brew가 MCP를 망가뜨리나? 바이너리가 x86_64이고 에이전트는 arm64일 때. file $(which node)를 맞추세요.
stdio가 멈추면? PATH는 exec, 파이프는 버퍼 — stdio 버퍼링.
npm_config_prefix·NPM_CONFIG_PREFIX를 plist에 넣고 전역 lib/node_modules를 잘못 읽는 경우가 있습니다. npm root -g로 전역 CLI 탐색과 PATH를 한 화면에 맞추세요. ProgramArguments에 공백이 있으면 셸과 launchd의 인자 분할이 달라질 수 있어 plutil -p로 확인하세요.
PATH 계약을 얼릴 곳, ProxyMac Mac mini
전용 Mac mini M4는 테넌트마다 /opt/homebrew를 NVMe에 굳히고, 남이 brew uninstall해도 Jenkins 실행이 덜 사라집니다. arm64·통합 메모리·5리전으로 Git plist를 동일 유지하며 복제하세요. PATH가 다시 지루해지면 병렬 에이전트, 가격으로 용량, 도움말, GUI는 VNC로.