폰에서 내 터미널 이어받기: cmux 원격 제어와 막힌 소켓을 뚫은 이야기
이동 중에도 작업하던 터미널을 폰에서 그대로 이어받고 싶었습니다. Tailscale로 폰과 Mac을 묶는 흔한 구성인데, cmux가 '자기가 띄운 프로그램만 들여보내는' 벽을 새로 세워 둔 탓에 막혔습니다. 그 과정과, Claude Code 공식 원격 기능과 둘을 같이 쓰는 이유까지 풀어 봅니다.
이동 중에 에이전트를 이어 보고 싶었다
코딩 에이전트를 책상에 돌려놓고 자리를 뜰 때가 많습니다. 빌드가 길거나, 테스트가 오래 돌거나, 에이전트가 중간에 “이렇게 할까요?” 하고 물어볼 때. 지하철에서, 걷다가, 그럴 때 폰에서 보고 “응 그렇게 해”라고 보낼 수 있으면 좋겠다는 게 시작이었습니다.
제 환경은 이렇습니다.
- 주력 노트북(Mac 1) — 평소 앞에 앉아 일하는 기기. 들고 다녀서 네트워크가 자주 끊겼다 붙고, 뚜껑도 여닫힙니다.
- 집에 두는 Mac 2 — 네트워크에 계속 연결된 채 상시 켜져 있습니다.
- 그리고 손에 든 아이폰.
긴 작업을 돌려놓고 폰에서 비동기로 이어 보는 대상은 Mac 2입니다. 상시 온라인이니까요. Mac 1은 제가 앞에 있을 때 쓰는 기기지만 네트워크가 들쭉날쭉해서, 자리를 떠도 살아 있어야 하는 작업은 집의 Mac 2로 보냅니다. 두 Mac은 설정을 공유하려고 홈 폴더를 동기화해 둡니다.
먼저, 왜 두 가지를 같이 쓰나
Claude Code에는 원격 제어 기능이 이미 들어 있습니다. 터미널에서 /remote-control을 치면 QR 코드나 링크가 뜨고, 폰의 Claude 앱이나 브라우저에서 그 세션을 이어받습니다. 코드는 계속 내 Mac에서 돌고, 폰은 그걸 들여다보는 창입니다. 설치랄 것도 없이 명령 하나. 저도 이걸 메인으로 잘 쓰고 있습니다.
그런데 이게 폰에 띄워 주는 건 “터미널”이 아니라 “Claude와의 대화창”입니다. 그래서 쓰다 보면 걸리는 게 있습니다.
- 터미널 전용 키가 안 먹습니다. 예를 들어 Claude Code에서
Esc를 두 번 누르면 이전 메시지로 돌아가는데, 폰 대화창에서는 안 됩니다. 대화창은 채팅 화면이지 진짜 터미널이 아니라서, 터미널이 받아 처리하던 키들이 전달될 길이 없습니다. - 붙는 데 시간이 걸릴 때가 있습니다. 세션을 서버에 등록하고 연결을 맺는 방식이라, 가끔 연결이 느릿했습니다.
- 요즘 들어 가끔 불안정했습니다. 시기 타는 부분이라 단정은 안 하겠습니다. 다만 손에 익은 비상 경로를 하나 더 갖고 싶었습니다.
- Claude 전용입니다.
/remote-control은 Claude Code에 묶여 있습니다. 저는 Codex도 같이 쓰는데, 그건 여기 안 보입니다.
그래서 진짜 터미널 화면을 그대로 비춰 주는 두 번째 길을 같이 둡니다. 저는 평소 터미널로 cmux를 씁니다(Manaflow가 만든 오픈소스 macOS 터미널입니다). cmux 화면을 폰에 그대로 미러링하면, 아까 안 되던 Esc Esc도 되고 아무 명령이나 칠 수 있습니다.
무엇보다 미러는 특정 에이전트가 아니라 터미널 자체를 비춥니다. 터미널은 Claude Code든 Codex든 다 그 위에서 도는 공통 바닥이라, 미러 한 화면이면 무엇을 띄웠든 똑같이 보입니다. 도구마다 따로 원격을 챙길 필요 없이 한 창에서 다 보는 셈이죠 — 흔히 말하는 single pane of glass입니다. /remote-control이 Claude에만 붙는 것과 달리, 미러는 도구를 안 가립니다.
두 길은 경쟁이 아니라 역할 분담입니다. 가볍게 한마디 던질 땐 /remote-control, 터미널을 제대로 만져야 할 땐 cmux 미러. 둘을 같이 켜 둡니다.
같은 일을 SSH + tmux로 푸는 길도 있습니다. 다른 서버까지 두루 들어가야 한다면 좋은 방법이지만, 저는 cmux를 쓰기 때문에 안 씁니다. “그런 길도 있다” 정도로만 적어 둡니다.
헷갈리는 이름부터 정리
두 번째 길을 만드는 조각들 이름이 비슷해서, 먼저 풀고 가겠습니다.
| 이름 | 정체 |
|---|---|
| cmux | 터미널 앱 본체 |
| cmux-remote | 오픈소스 프로젝트(저장소) 이름 |
| cmux-relay | 그 프로젝트가 만드는 중계 프로그램. 항상 켜져 있도록 백그라운드로 띄웁니다 |
| Cmux Remote | App Store에 있는 아이폰 앱 |
폰의 Cmux Remote 앱이, 멀리 있는 **cmux-relay(중계 프로그램)**에 붙고, 그 중계기가 내 Mac의 cmux에 연결해 주는 구조입니다.
그림으로 보면
flowchart LR phone["폰<br/>(Cmux Remote 앱)"] relay["cmux-relay<br/>(중계 프로그램)"] cmux["cmux<br/>(터미널 본체)"] phone -- "(A) Tailscale 사설망" --> relay relay -- "(B) 같은 Mac 안 연결" --> cmux두 개의 연결이 있습니다.
- (A) 폰 ↔ 중계기: Tailscale로 묶습니다. Tailscale은 내 기기들끼리만 통하는 사설망을 만들어 주는 도구입니다. 같은 계정이면 폰이 자동으로 인증되고, 트래픽은 내 기기들 사이에서만 오갑니다. 공인 IP를 열거나 포트 포워딩 할 필요가 없습니다.
- (B) 중계기 ↔ cmux: 같은 Mac 안에서의 연결입니다. 여기서 막혔습니다.
설치와 Syncthing 함정
프로젝트를 받아 설치 스크립트를 돌리면, 중계 프로그램이 빌드되고 로그인 시 자동으로 켜지도록 등록됩니다. 상태를 물어보면 정상이라고 답합니다.
git clone https://github.com/NewTurn2017/cmux-remote.gitcd cmux-remote && ./scripts/install-launchd.sh # 빌드 + 자동시작 등록curl -s http://127.0.0.1:4399/v1/health # {"ok":true}곁가지 함정이 하나 있었습니다. 저는 두 Mac의 홈 폴더를 Syncthing으로 통째 동기화합니다. Syncthing은 기기들 사이에서 폴더를 똑같이 맞춰 주는 도구입니다. 그런데 중계 프로그램은 두 Mac에서 각자 돌면서 같은 위치(~/.cmuxremote/log/...)에 로그를 씁니다. Syncthing 입장에선 같은 파일이 양쪽에서 동시에 바뀌는 셈이라, 어느 쪽을 살릴지 못 정하고 한쪽을 .sync-conflict-... 사본으로 떨궈 둡니다. 두 중계기가 계속 로그를 쓰니 그 사본이 계속 쌓입니다. 해결은 이 런타임 폴더(/.cmuxremote)와 빌드 결과물(.build)을 Syncthing 제외 목록(.stignore)에 넣어 동기화에서 빼는 것이었습니다.
이건 cmux만의 문제가 아니라, Syncthing처럼 여러 대를 동기화로 묶을 때 한 번은 꼭 부딪히는 지점입니다. “공유할 것”과 “기기마다 다를 것”을 갈라 줘야 합니다.
상태는 정상이라는데, 폰에서 연결하니 아무 화면도 안 뜹니다.
증상: 8초마다 끊겼다 붙었다
중계기 로그를 봤더니 연결이 규칙적으로 붙었다 떨어집니다.
연결됨끊김; 다시 시도연결됨끊김; 다시 시도정확히 8초 간격이었습니다. 폰으로 가는 알림이 이 연결을 타고 흐르는데, 8초마다 끊기는 길로는 폰이 “연결 끊김”만 봅니다.
여기서 첫 교훈. “상태 정상”이라는데 동작은 안 됐습니다. 그 상태 점검은 중계기 자신이 살아 있는지만 봤지, 중계기와 cmux 사이의 (B) 연결은 안 봤습니다. “정상”이라는 신호가 실제로 무엇을 확인한 건지부터 따져야 했습니다.
두 번 헛다리를 짚었다
진짜 원인으로 가기 전에 두 번 엉뚱한 데를 팠습니다. 같은 길로 들어설 사람을 위해 남겨 둡니다.
헛다리 1 — 버전 차이인가? 중계기 코드 주석에 옛날 cmux 버전 이야기가 있었습니다. 제 cmux는 더 최신이라 “버전이 안 맞나” 의심했는데, 핵심은 아니었습니다.
헛다리 2 — 미러 기능 자체가 깨진 건가? 코드를 더 읽어 보니 통로가 둘로 나뉘어 있었습니다. 하나는 화면 미러와 키 입력용, 다른 하나는 알림용. 끊기는 건 알림 통로뿐이라, 화면 미러는 멀쩡할 수도 있었습니다. 증상의 뿌리는 아니었습니다.
진짜 원인은 로그의 한 줄에 그대로 적혀 있었습니다.
접근 거부 — cmux 안에서 시작된 프로그램만 연결할 수 있습니다진짜 원인: cmux는 “누가 켰는지”를 따진다
cmux 최신 버전은, 자기 소켓에 연결하려는 프로그램이 자기가 직접 띄운 것인지 확인합니다. 소켓은 같은 컴퓨터 안에서 프로그램끼리 대화하는 통로라고 보면 됩니다.
원리는 단순합니다. 모든 프로그램에는 그걸 실행시킨 “부모”가 있습니다. 그 부모에게도 또 부모가 있고요. cmux는 연결해 온 프로그램의 부모를, 또 그 부모의 부모를 거슬러 올라가다가 그 위에 cmux 자신이 있으면 “내가 켠 거네” 하고 들여보내고, 없으면 막습니다.
왜 이런 걸 넣었을까요. 이 통로로 들어오면 터미널을 마음대로 조종할 수 있습니다. 그러니 같은 컴퓨터에서 도는 아무 프로그램이나 들어오지 못하게, “내가 직접 띄운 것(=cmux 창 안에서 실행된 것)만”으로 잠가 둔 겁니다. 합리적인 보안 장치입니다.
문제는 제 중계 프로그램이 cmux와 상관없이 따로 떠 있는 백그라운드 프로그램이라는 점입니다. 부모를 거슬러 올라가도 cmux가 안 나오니, 계속 거부됩니다. “내가 띄운 것만 허용”하는 cmux와, “따로 도는 중계기”는 애초에 안 맞습니다.
이걸 어떻게 확인했나 — 깨끗한 셸로 비교하기
심증만으로는 부족하니 갈라서 확인했습니다. 어디서나 써먹을 수 있는 요령이 나옵니다.
같은 명령을 (1) cmux 창 안에서 한 번, (2)
ssh localhost로 새로 들어간 셸에서 한 번 — 두 군데서 돌려 비교합니다.
ssh localhost는 내 컴퓨터에 다시 로그인해 들어가는 건데, 이렇게 들어간 셸은 cmux가 아니라 로그인 시스템이 띄운 거라 cmux와 무관한 “깨끗한” 셸입니다.
# cmux 창 안에서cmux rpc workspace.list '{}' # 정상 — 결과가 나옴
# ssh localhost (cmux 밖)ssh localhost "cmux ping" # 즉시 연결 끊김같은 컴퓨터, 같은 사용자, 같은 명령인데 누가 띄운 셸이냐만으로 갈렸습니다. 이 비교법은 cmux에만 쓰는 게 아닙니다. “이 거부가 권한 문제인가, 아니면 누가 띄웠느냐의 문제인가”가 헷갈릴 때, 그 앱이 안 띄운 깨끗한 셸에서 같은 걸 돌려 보면 갈립니다.
푸는 법: cmux 쪽 문을 열어 준다
중계기가 따로 도는 프로그램인 이상, cmux 쪽에서 “밖에서 오는 연결도 받겠다”고 바꿔 줘야 합니다. cmux 설정에 소켓 접근 모드가 있습니다.
| 모드 | 뜻 |
|---|---|
| 기본값 | cmux가 직접 띄운 것만 허용 — 우리를 막던 그것 |
| 비밀번호 | 비밀번호를 아는 프로그램이면 밖에서도 허용 |
| 전체 허용 | 아무나 허용 (그 컴퓨터 안 한정) |
비밀번호 모드가 더 안전하지만, 머신마다 설정 화면에서 일일이 비밀번호를 넣어야 하고 뒤에 나올 함정도 겹칩니다. 전체 허용은 한 줄로 끝나고 비밀번호도 필요 없습니다.
“전체 허용이면 위험하지 않냐”고요? 여기서 열리는 건 그 Mac 안의 통로뿐입니다. 밖(폰)에서 들어오는 길은 여전히 Tailscale 사설망 + 기기 인증으로 막혀 있습니다. 혼자 쓰는 개인 Mac이라면 받아들일 만하고, 여럿이 쓰는 컴퓨터라면 비밀번호 모드가 맞습니다. 저는 한 대는 비밀번호, 한 대는 전체 허용으로 두고 둘 다 확인했습니다.
함정 둘 — 비밀번호 파일 위치가 어긋나 있었다
비밀번호 모드를 골랐다면 함정이 하나 더 있습니다. 최신 cmux는 비밀번호를 새 위치에 저장하는데, 중계 프로그램은 옛 버전 기준으로 만들어져서 옛 위치에서 찾습니다. 그래서 비밀번호가 있는데도 중계기가 못 찾습니다. 옛 위치가 새 위치를 가리키도록 바로가기(심볼릭 링크) 하나로 이어 줬습니다.
빠르게 바뀌는 앱에 다른 사람이 만든 도구를 붙일 때 흔한 일입니다. 한쪽이 옮긴 파일을 다른 쪽이 옛 자리에서 찾는 부류죠.
함정 셋 — 설정을 바꿨는데 안 먹는다
모드를 바꿨는데도 깨끗한 셸 테스트는 그대로 막혀 있었습니다. 설정 다시 읽기 명령을 줘도 소용없었고요. 이유는, 실행 중인 cmux는 켜질 때 읽은 설정을 기억하고 있어서, 파일만 바꾼다고 바로 반영되지 않기 때문입니다. 결국 cmux를 한 번 껐다 켜야 새 설정을 받았습니다.
“설정을 바꿨는데 안 먹는다”는 거의 항상 이 신호입니다. 저장된 값과, 돌고 있는 프로그램이 기억하는 값은 별개라는 것. 마지막 카드가 “앱 재시작”인 이유입니다. 다만 cmux를 끄면 그 안에서 돌던 작업(에이전트 포함)도 같이 꺼지니, 정리하고 나서 해야 합니다.
껐다 켜자 깨끗한 셸 테스트가 바로 통과했고, 8초마다 끊기던 로그도 멎었습니다.
폰에서, 그리고 다른 기기까지
폰 앱에서는 주소 칸에 Tailscale 주소를 넣습니다. 짧은 컴퓨터 이름은 앱이 거부하고, Tailscale이 주는 IP나 전체 주소를 넣어야 통과합니다. 같은 Tailscale 계정이면 자동으로 짝이 맞춰집니다. 두 번째 Mac도 똑같이 한 번 더 해 주면, 폰에서 연결 대상만 바꿔 두 Mac을 오갈 수 있습니다.
다른 애플 기기는 어떨까요 (Cmux Remote 앱 기준).
- 아이폰 — 됩니다.
- Mac — 됩니다. 한 Mac에서 다른 Mac의 cmux를 이 앱으로 볼 수도 있습니다.
- 아이패드 — 아직 안 됩니다. 앱이 아이패드용을 안 냈습니다. 아이패드에서 꼭 써야 하면 cmux 미러 대신
/remote-control(Claude 앱)이나 일반 SSH 앱으로 우회해야 합니다.
끊겨도 작업은 안 죽는다
사실 저한테 제일 컸던 건 이 부분입니다. 주력 노트북(Mac 1)은 들고 다니다 보니 네트워크가 자주 끊기고, 핫스팟으로 잡을 때도 많습니다. 그래서 회의에 들어가거나 화장실에 갈 때 폰을 들고 나가는 순간 Mac 1이 인터넷을 잃고, 돌아가던 에이전트가 멈춰 버립니다. 자리를 못 뜨게 만드는 거죠.
그래서 자리를 떠도 살아 있어야 하는 긴 작업은 집에 상시 연결돼 있는 Mac 2에 둡니다. 폰은 Mac 2의 세션에 비동기로 붙는 창일 뿐입니다. 내 노트북(Mac 1)이 닫히든 네트워크가 끊기든 Mac 2는 영향을 안 받으니, 회의에 들어가며 폰을 빼도 집의 에이전트는 계속 돌고, 폰은 셀룰러로 언제든 다시 그 세션을 들여다봅니다. 작업이 연결이 아니라 늘 깨어 있는 기기에 살아 있는 거죠. tmux를 써 본 분이라면 익숙한 원리입니다.
두 트랙의 차이도 여기서 드러납니다. 둘 다 끊겼다 돌아오면 다시 이어지지만, 체감 속도가 다릅니다. /remote-control은 다시 탭하면 살아나는데 붙기까지 한 박자 걸리고, cmux 미러는 빠르게 다시 붙습니다. 자주 끊겼다 붙는 환경(핫스팟, 이동 중)일수록 이 차이가 쌓입니다.
나는 이렇게 쓴다
긴 작업은 집에 켜 둔 Mac 2에 돌려놓습니다. 그러면 노트북(Mac 1)을 닫고 나가도, 지하철이든 소파든 약속 자리에서 짬이 나든 폰으로 그 세션을 이어 봅니다. 자리를 안 지켜도 된다는 것, 이게 제일 큰 변화였습니다.
폰에서 하는 일은 대부분 가볍습니다. 진행을 보고 승인하거나 거절하고(“응 그렇게 해” / “아니 그건 빼”), 짧게 다음 지시를 넣고, 출력이나 에러 로그를 확인하는 정도. 여기까지는 /remote-control로 충분합니다. 터미널을 직접 봐야 하거나 Esc Esc 같은 키가 필요할 때만 cmux 미러로 넘어갑니다. 급하면 폰에서 바로 개입해 멈추거나 방향을 틀고, 아니면 그냥 놔둡니다.
Claude Code와 Codex를 오가며 쓰는데, 미러로 보면 둘이 같은 창이라 지금 뭐가 돌고 있든 신경 쓸 일이 없습니다.
물론 폰으로 오래 붙들고 있지는 않습니다. 잠깐 보고 넘기는 용도지, 폰으로 코딩하겠다는 건 아니니까요.
관련 콘텐츠
Claude Code 스킬 중앙 관리: 심볼릭 링크로 깔끔하게
늘어나는 Claude Code 스킬 저장소들을 한 곳에서 관리하고, 프로젝트별로 필요한 스킬만 연결하는 방법.
productivityClaude Code 대화 기록 분석으로 스킬 자동화 기회 찾기
로컬에 쌓이는 Claude Code 대화 기록을 컨텍스트로 활용해 반복 패턴을 찾고, 이를 Claude Skill로 자동화하는 방법.
productivityClaude Code로 포트폴리오 + 기술 블로그 운영하기
AI 코딩 에이전트와 협업하여 포트폴리오 + 기술 블로그를 구축한 경험을 공유합니다. 효과적인 협업 방식, 자동화 시스템 구현, 운영 워크플로우까지 다룹니다.