tmux에서 Claude Code 드래그 복사가 안 될 때 — OSC 52와 allow-passthrough

tmux에서 Claude Code 드래그 복사가 안 될 때 — OSC 52와 allow-passthrough SSH로 원격 서버에 붙어 tmux 세션에서 Claude Code(또는 다른 TUI)를 쓰다 보면 이런 경험을 한다. 출력된 텍스트를 마우스로 드래그하면 화면 하단에 이런 알림이 뜬다. sent 196 chars via…

tmux에서 Claude Code 드래그 복사가 안 될 때 — OSC 52와 allow-passthrough

SSH로 원격 서버에 붙어 tmux 세션에서 Claude Code(또는 다른 TUI)를 쓰다 보면 이런 경험을 한다. 출력된 텍스트를 마우스로 드래그하면 화면 하단에 이런 알림이 뜬다.

sent 196 chars via OSC 52 · check terminal clipboard settings if paste fails

그런데 막상 로컬 머신에 붙여넣으면 클립보드가 비어 있다. 터미널을 Windows Terminal, VSCode 통합 터미널, Warp로 바꿔봐도 똑같다. 터미널 엔진 문제인가 싶지만 — 아니다. 범인은 tmux의 allow-passthrough 설정이었다.

이 글은 이 문제를 단계적으로 좁혀나간 디버깅 과정과 최종 해결책을 정리한다.

한 줄 해결: 원격 쉘은 되는데 tmux 안의 TUI에서만 복사가 안 된다면, ~/.tmux.confset -g allow-passthrough on을 추가하라. Claude Code 같은 TUI의 DCS 패스스루 복사는 이 한 줄이면 동작한다. (set-clipboard on은 일반 쉘의 raw OSC 52 복사에 필요한 별도 옵션이니 함께 켜두면 좋다.) 원인과 검증 과정이 궁금하면 아래 1~3단계를, 바로 점검만 하고 싶으면 맨 아래 디버깅 체크리스트로 가면 된다.

터미널 하단에 OSC 52 클립보드 알림이 뜬 모습 — 복사 시도가 조용히 클립보드에 닿지 않은 상황

OSC 52란 무엇인가?

OSC 52는 터미널 이스케이프 시퀀스의 하나로, 원격 호스트의 프로그램이 로컬 터미널의 시스템 클립보드에 직접 쓸 수 있게 해주는 메커니즘이다. SSH로 접속한 원격 서버에서 xclip이나 pbcopy가 동작하지 않아도, OSC 52를 지원하는 터미널이라면 클립보드 복사가 가능하다.

형식은 단순하다.

ESC ] 52 ; c ; <base64로 인코딩된 텍스트> BEL
  • 52 — OSC 52 식별자
  • c — 대상 클립보드 (c=CLIPBOARD, p=PRIMARY)
  • base64 페이로드
  • BEL(a) 또는 ST(ESC )로 종결

쉘에서 직접 쏘면 이렇게 된다.

printf '33]52;c;%sa' "$(printf 'hello' | base64 | tr -d 'n')"

이 한 줄을 실행하면 OSC 52를 지원하는 터미널이 hello를 시스템 클립보드에 넣는다. tr -d 'n'을 붙인 이유는 GNU base64(Linux 기본)가 76자마다 줄바꿈을 삽입하기 때문이다. 페이로드 중간에 줄바꿈이 들어가면 이스케이프 시퀀스가 끊겨 복사가 실패하므로, base64 출력의 개행을 반드시 제거해야 한다(macOS는 base64 -b 0도 가능).

1단계: 터미널을 의심했지만 무죄였다

알림이 “check terminal clipboard settings”라고 하니 터미널 설정부터 의심하는 게 자연스럽다. 그래서 터미널을 거치지 않고 OSC 52 원시 시퀀스만 단독으로 테스트했다.

printf '33]52;c;%sa' "$(printf 'osc52-test' | base64 | tr -d 'n')"

실행 후 메모장에 붙여넣기 → osc52-test가 정상적으로 들어왔다.

이 결과가 핵심 분기점이다. 터미널과 tmux를 포함한 OSC 52 경로 자체는 멀쩡하다는 뜻이다. 그렇다면 Claude Code가 보내는 OSC 52만 어딘가에서 막히고 있다.

교훈 1: “터미널 설정을 확인하라”는 에러 메시지를 곧이곧대로 믿지 말 것. 원시 시퀀스 단독 테스트로 계층을 분리하는 게 먼저다.

2단계: 세 터미널 모두 동일하다면 공통 레이어가 범인

Windows Terminal, VSCode 통합 터미널, Warp — 셋 다 같은 증상이었다. 서로 완전히 다른 터미널 엔진인데 동일하게 실패한다면, 문제는 터미널이 아니라 그 아래 공통 레이어에 있다. 공통 요소는 SSH → tmux → Claude Code다.

여기서 또 하나의 단서가 보였다. 드래그할 때 화면에 이런 쓰레기 문자가 새어나왔다.

35;227;70M

이건 SGR 마우스 이벤트(<버튼;열;행 M)가 소비되지 못하고 글자로 출력된 것이다. tmux의 mouse on과 Claude Code의 마우스 처리가 충돌하면서 시퀀스가 깨끗하게 전달되지 못하고 있다는 신호였다.

3단계: pane을 분리한 결정적 실험

가장 결정적인 실험은 Claude Code가 도는 pane이 아닌, 같은 tmux 세션의 다른 pane(일반 쉘)에서 동일한 OSC 52 테스트를 돌리는 것이었다.

# tmux의 일반 쉘 pane에서 실행
printf '33]52;c;%sa' "$(printf 'in-tmux-test' | base64 | tr -d 'n')"

결과: in-tmux-test가 클립보드에 정상 도착.

이걸로 모든 게 확정됐다.

경로 OSC 52 복사
쉘 → 터미널 (tmux 없음) 성공
tmux 일반 pane → 터미널 (raw OSC 52) 성공
tmux 내 Claude Code pane → 터미널 (DCS 패스스루) 실패

여기서 주의할 점. 이 결과로 검증된 것은 set-clipboard on 기반의 raw OSC 52 경로가 정상이라는 사실뿐이다. Claude Code가 쓰는 DCS 패스스루 경로는 오히려 막혀 있었고, 그게 바로 다음 절에서 밝혀질 근본 원인이다. 즉 같은 pane이라도 OSC 52를 어떤 방식으로 보내느냐에 따라 운명이 갈렸다.

tmux에서 복사가 안 되는 근본 원인은 무엇인가?

왜 일반 쉘은 되고 Claude Code만 안 될까? 둘이 OSC 52를 보내는 방식이 다르기 때문이다. 같은 OSC 52라도 tmux를 만났을 때 가로채기와 통과로 갈라지는 지점이 있다.

구분 일반 쉘 printf Claude Code (TUI)
전송 방식 raw OSC 52 그대로 DCS 패스스루로 감쌈
tmux 처리 set-clipboard on이 가로채 외부로 재전송 allow-passthrough 켜져야 통과
조건 미충족 시 — (항상 동작) tmux가 시퀀스를 먹어버림 → 실패
트리거 사용자 직접 실행 $TMUX 환경변수 감지
  • 일반 쉘 printf는 날것(raw) 그대로의 OSC 52를 송신한다. tmux의 set-clipboard on이 이를 가로채 자기 버퍼에 저장하고 외부 터미널로 재전송한다.
  • Claude Code는 환경변수 $TMUX를 감지하면, OSC 52를 tmux DCS 패스스루 시퀀스로 감싸서 보낸다.

여기서 표기에 함정이 있다. tmux 패스스루는 감싸는 안쪽의 ESC를 모두 두 번씩(이중화) 써야 한다.

ESC P tmux ; ESC ESC ] 52 ; c ; <base64> BEL ESC 

즉 내부의 33]52;...33Ptmux;3333]52;...33\ 형태로 들어간다. 안쪽 ESC를 이중화하지 않으면 tmux가 패스스루 내용을 올바로 복원하지 못한다(따라 쓸 때 흔히 틀리는 부분이다). 이렇게 감싸는 이유는 tmux의 클립보드 처리를 거치지 않고 시퀀스를 그대로 외부 터미널까지 통과시키기 위해서다. 그런데 이 패스스루가 동작하려면 tmux 옵션 allow-passthrough가 켜져 있어야 한다.

확인해보니 이렇게 나왔다.

$ tmux show-options -g allow-passthrough
allow-passthrough off

off였다. allow-passthrough는 tmux 3.3에서 도입됐고, 최근 tmux에서는 보안상 꺼짐이 기본이다(정확한 도입·기본값 이력은 tmux 공식 man 페이지와 CHANGES를 참고). 그래서 Claude Code가 감싸 보낸 DCS 패스스루 시퀀스를 tmux가 그냥 먹어버려 클립보드까지 닿지 못한 것이다. 반면 일반 쉘의 raw OSC 52는 set-clipboard on 경로를 타기 때문에 멀쩡히 동작했다.

교훈 2: TUI마다 tmux 안에서의 클립보드 전략이 다르다. raw OSC 52를 쓰는 도구와 DCS 패스스루로 감싸는 도구가 공존한다.

어떻게 해결하나? allow-passthrough on

해결은 .tmux.conf에 한 줄을 추가하는 것이다. DCS 패스스루를 허용하면 Claude Code가 감싸 보낸 OSC 52가 tmux에 먹히지 않고 외부 터미널까지 그대로 통과해 클립보드에 쓰인다. set-clipboard on이 raw 경로를 책임진다면, allow-passthrough on은 패스스루 경로를 열어주는 짝꿍인 셈이다.

# DCS 패스스루 허용. Claude Code 등 일부 TUI는 $TMUX 감지 시 OSC 52를
# tmux 패스스루(33Ptmux;...)로 감싸 보내는데, off면 그 시퀀스가 먹혀 복사 실패.
set -g allow-passthrough on

적용 후(tmux source-file ~/.tmux.conf 또는 prefix + r), Claude Code에서 드래그 복사가 정상 동작했다.

보안 트레이드오프는 알고 켜자

allow-passthrough의 기본값이 꺼짐인 데는 이유가 있다. 켜면 pane 안에서 도는 임의의 프로그램이 tmux를 우회해 외부 터미널로 escape 시퀀스를 직접 주입할 수 있게 된다. 신뢰할 수 없는 출력(예: cat으로 정체불명의 파일을 출력)이 외부 터미널의 동작을 건드릴 여지가 생긴다는 뜻이다. 본인이 쓰는 도구(Claude Code 등)가 명확하고 출력 소스를 신뢰할 수 있는 개인 작업 환경이라면 켜도 무방하지만, 공용 서버나 신뢰할 수 없는 출력을 자주 다루는 환경이라면 위험을 인지하고 결정하는 게 좋다.

allow-passthrough on 적용 전후 비교 — 좌측은 복사 실패(끊긴 연결), 우측은 복사 성공(연결된 흐름)

알림 메시지는 계속 뜬다 — 정상이다

한 가지 주의할 점. 복사가 성공해도 sent N chars via OSC 52 ... 알림은 계속 뜬다. 이건 Claude Code가 OSC 52를 송신할 때마다 띄우는 정보성 안내문이지 에러가 아니다. 실제 붙여넣기가 되면 무시해도 된다.

보너스: tmux 클립보드 헬퍼 스크립트는 필요했나?

이 과정에서 기존에 쓰던 복잡한 클립보드 헬퍼 스크립트(copy-pipewl-copyxclip에 흘리는 방식)를 점검했다. 결론은 헤드리스 원격에서는 대부분 무의미했다는 것이다. wl-copyxclip은 로컬 DISPLAY나 Wayland 소켓이 있어야 동작하는데, SSH 원격 서버엔 그게 없다. 결국 원격 클립보드를 책임지는 건 set-clipboard on의 OSC 52뿐이었다.

그래서 마우스 복사 바인딩을 tmux 내장 명령으로 단순화했다.

# 외부 스크립트 대신 내장 copy-selection 사용.
# set-clipboard on 덕에 OSC 52로 외부(원격 포함) 클립보드까지 전달된다.
bind -T copy-mode-vi MouseDragEnd1Pane send-keys -X copy-selection-no-clear ; send -X clear-selection

set-clipboard onterminal-features '*:clipboard' 조합이면 별도 헬퍼 없이 OSC 52가 원격 클립보드까지 전달된다.

정리: 클립보드 복사 디버깅 체크리스트

tmux와 원격 환경에서 클립보드 복사가 안 될 때 순서대로 확인하자.

  1. 원시 OSC 52 단독 테스트 — printf '33]52;c;%sa' "$(printf test | base64 | tr -d 'n')". 되면 터미널과 tmux는 무죄다. (base64 개행 제거 필수)
  2. 여러 터미널에서 동일한가 — 동일하면 터미널이 아니라 공통 하위 레이어(tmux나 앱) 문제다.
  3. 앱 pane과 일반 pane 비교 — 일반 pane만 되면 특정 앱(TUI)의 OSC 52 방식 문제다.
  4. tmux show-options -g allow-passthroughoff면 DCS 패스스루를 쓰는 TUI의 클립보드가 막힌다. set -g allow-passthrough on으로 해결.
  5. set-clipboard onterminal-features '*:clipboard' — raw OSC 52 경로를 위해 필요하다.
  6. 그래도 안 되면 두 가지 한계를 의심하라. (1) OSC 52는 base64 페이로드 크기 제한이 있어 아주 큰 선택은 잘리거나 실패할 수 있다. (2) 일부 터미널은 OSC 52 클립보드 쓰기가 기본 비활성이라 앱(터미널) 설정에서 명시적으로 허용해야 한다.

마치며

“터미널 설정을 확인하라”는 에러 메시지에 낚여 세 개의 터미널을 갈아탈 뻔했지만, 정작 답은 .tmux.conf 한 줄이었다. 계층을 분리하는 단독 테스트(raw OSC 52, pane 분리)가 없었다면 한참을 헤맸을 것이다. 멀티 레이어 환경(SSH + tmux + TUI)의 디버깅은 결국 어느 계층에서 끊기는지를 이분 탐색하는 게 핵심이다.

tmux 안에서 Claude Code 같은 TUI의 OSC 52 클립보드 복사가 안 되면, 가장 먼저 set -g allow-passthrough on을 의심하라.

참고 링크

  • tmux 공식 man 페이지 (allow-passthrough, set-clipboard 옵션): https://man.openbsd.org/tmux
  • tmux CHANGES (옵션 도입 이력): https://github.com/tmux/tmux/blob/master/CHANGES
  • XTerm Control Sequences — OSC 52 (Manipulate Selection Data): https://invisible-island.net/xterm/ctlseqs/ctlseqs.html

관련 글

@welltip

정리를 위해 작성하는 개인노트