증상
Claude Code를 v2.1.84로 업그레이드한 후 두 가지 문제가 발생했다.
1. Caps Lock 입력 시 escape sequence 노출
Caps Lock(한/영 전환)을 누르면 [57358u라는 escape sequence가 화면에 그대로 출력된다.
❯ [57358u[57358uasdfasdf[57358u[57358uㄴㅁㅇㄹ
2. 터미널 폭 오인식
실제 터미널 폭과 관계없이 약 50칸럼으로 렌더링되어 텍스트가 비정상적으로 줄바꿈된다.
원인
Kitty Keyboard Protocol
Claude Code는 시작 시 \x1b[>1u escape sequence를 보내 터미널을 Kitty Keyboard Protocol 모드로 전환한다.
- Kitty protocol에서 Caps Lock의 keycode는 57358
- Claude Code의 키 파서가 이 keycode를 처리하지 못해
undefined를 반환 undefined가 raw text로 취급되어 escape sequence가 화면에 그대로 노출
이 프로토콜은 터미널에서 Cmd+V와 Ctrl+V 등 modifier 키 조합을 정확히 구분하는 데 사용된다. 이 점이 v1.1.0 변경의 핵심 배경이다.
PTY 크기 버그
- Python의
os.get_terminal_size()는(columns, lines)순서로 반환 - PTY wrapper가
rows, cols로 역순 언패킹하여 폭과 높이가 바뀜 - 결과적으로 터미널 높이 값이 폭으로 사용되어 약 50칸럼으로 인식
해결 방법
1. PTY Proxy Wrapper (핵심)
~/.local/bin/claude-wrapper를 작성하여 Claude Code의 stdout을 실시간 필터링한다.
v1.0.x: Kitty Protocol 전체 비활성화
최초 접근은 stdout에서 \x1b[>1u (kitty push) 시퀀스를 통째로 제거하는 방식이었다.
# v1.0.x
KITTY_PUSH = b'\x1b[>1u'
stdout_buf = stdout_buf.replace(KITTY_PUSH, b'', 1)
이 방식은 한/영 문제를 해결하지만, kitty protocol 자체가 비활성화되어 부작용이 발생했다:
- Kitty protocol이 비활성화되어 Claude Code가 키 이벤트를 정확히 구분하지 못함
- Ctrl+V 이미지 붙여넣기 불가 (이미지 데이터가 Claude Code에 전달되지 않음)
- Cmd+V는 iTerm2가 macOS 앱 레벨에서 가로채므로 터미널 앱에 원래 전달되지 않음
v1.1.0: CapsLock 키코드만 선택적 필터링 (현재)
kitty protocol은 유지하면서, 문제의 원인인 CapsLock 키코드만 정규식으로 필터링한다.
# v1.1.0
CAPSLOCK_PATTERN = re.compile(rb'\x1b\[57358(;\d+)*u')
stdout_buf = CAPSLOCK_PATTERN.sub(b'', stdout_buf)
v1.0.x → v1.1.0 동작 비교:
# v1.0.x: kitty protocol 전체 제거
사용자 ← stdout ← [ESC[>1u 제거] ← [claude]
(legacy 모드 → Ctrl+V 이미지 붙여넣기 불가)
# v1.1.0: CapsLock 키코드만 제거
사용자 ← stdout ← [57358u만 제거] ← [claude]
(kitty protocol 유지 → Ctrl+V 이미지 붙여넣기 정상)
래퍼 코드 (v1.1.0)
#!/usr/bin/env python3
"""Claude Code wrapper - CapsLock 한/영 전환 키코드 필터.
Changelog:
v1.0.0 (2026-03-26): kitty keyboard protocol 전체 비활성화 (ESC[>1u 제거)
v1.0.1 (2026-03-26): PTY 크기 전달 버그 수정 — ioctl TIOCGWINSZ 기반으로 교체
v1.0.2 (2026-03-28): SIGWINCH 핸들러 안정화, set_winsize() ioctl 통합
v1.1.0 (2026-03-30): kitty protocol 유지, CapsLock(57358u) 키코드만 필터링
→ Ctrl+V 이미지 붙여넣기 정상화
"""
__version__ = "1.1.0"
import os
import re
import sys
import pty
import signal
import select
import struct
import fcntl
import termios
CAPSLOCK_PATTERN = re.compile(rb'\x1b\[57358(;\d+)*u')
def get_real_claude():
"""실제 claude 바이너리 경로 반환."""
for path in [
'/opt/homebrew/bin/claude',
'/usr/local/bin/claude',
]:
if os.path.exists(path):
return path
versions_dir = os.path.expanduser('~/.local/share/claude/versions')
if os.path.isdir(versions_dir):
versions = sorted(os.listdir(versions_dir))
if versions:
return os.path.join(versions_dir, versions[-1])
return None
def set_winsize(fd):
"""터미널 윈도우 크기를 pty에 전달."""
try:
s = struct.pack('HHHH', 0, 0, 0, 0)
result = fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, s)
fcntl.ioctl(fd, termios.TIOCSWINSZ, result)
except (OSError, AttributeError):
pass
def main():
claude_bin = get_real_claude()
if not claude_bin:
print("Error: Claude Code binary not found", file=sys.stderr)
sys.exit(1)
pid, master_fd = pty.fork()
if pid == 0:
os.execvp(claude_bin, [claude_bin] + sys.argv[1:])
sys.exit(1)
set_winsize(master_fd)
def handle_winch(signum, frame):
set_winsize(master_fd)
os.kill(pid, signal.SIGWINCH)
signal.signal(signal.SIGWINCH, handle_winch)
old_settings = None
try:
old_settings = termios.tcgetattr(sys.stdin.fileno())
except termios.error:
pass
try:
if old_settings:
import tty
tty.setraw(sys.stdin.fileno())
stdout_buf = b''
while True:
try:
rlist, _, _ = select.select(
[sys.stdin.fileno(), master_fd], [], [], 0.1
)
except (select.error, ValueError):
break
if sys.stdin.fileno() in rlist:
try:
data = os.read(sys.stdin.fileno(), 4096)
if not data:
break
os.write(master_fd, data)
except OSError:
break
if master_fd in rlist:
try:
data = os.read(master_fd, 4096)
if not data:
break
stdout_buf += data
if stdout_buf.endswith(b'\x1b'):
continue
stdout_buf = CAPSLOCK_PATTERN.sub(b'', stdout_buf)
os.write(sys.stdout.fileno(), stdout_buf)
stdout_buf = b''
except OSError:
break
if stdout_buf:
stdout_buf = CAPSLOCK_PATTERN.sub(b'', stdout_buf)
os.write(sys.stdout.fileno(), stdout_buf)
finally:
if old_settings:
termios.tcsetattr(
sys.stdin.fileno(), termios.TCSADRAIN, old_settings
)
try:
_, status = os.waitpid(pid, 0)
sys.exit(os.WEXITSTATUS(status) if os.WIFEXITED(status) else 1)
except ChildProcessError:
pass
if __name__ == '__main__':
main()
설정:
# ~/.zshrc에 alias 추가
alias claude='python3 ~/.local/bin/claude-wrapper'
참고: iTerm2에서 이미지 붙여넣기는 Cmd+V가 아닌 Ctrl+V로 동작한다. Cmd+V는 iTerm2가 macOS 앱 레벨에서 가로채 “터미널 텍스트 붙여넣기"로 처리하므로, 터미널 안의 Claude Code까지 도달하지 않는다. 이미지 붙여넣기는 Ctrl+V를 사용해야 한다. VS Code 터미널에서는 자체 clipboard API를 사용하므로 Cmd+V로도 이미지 붙여넣기가 가능하다.
2. Zsh Hook (안전망)
Claude Code가 비정상 종료하더라도 Kitty protocol이 남아있지 않도록 precmd hook을 설정한다.
_disable_kitty_protocol() {
printf '\e[<u\e[<u\e[<u\e[<u\e[<u'
}
autoload -Uz add-zsh-hook
add-zsh-hook precmd _disable_kitty_protocol
매 프롬프트 표시 전에 Kitty protocol pop sequence를 여러 번 전송하여, 쌓여 있을 수 있는 push 상태를 모두 해제한다.
3. Claude Code 설정 정리
~/.claude/settings.json에서 문제를 일으키는 statusLine hook을 제거한다. 불필요한 hook이 터미널 출력에 간섭할 수 있으므로 정리가 필요하다.
버전 이력
| 버전 | 날짜 | 변경 |
|---|---|---|
| v1.0.0 | 2026-03-26 | kitty keyboard protocol 전체 비활성화 (ESC[>1u 제거) |
| v1.0.1 | 2026-03-26 | PTY 크기 전달 버그 수정 — ioctl TIOCGWINSZ 기반으로 교체 |
| v1.0.2 | 2026-03-28 | SIGWINCH 핸들러 안정화, set_winsize() ioctl 통합 |
| v1.1.0 | 2026-03-30 | kitty protocol 유지, CapsLock 57358u만 필터링 → Ctrl+V 이미지 붙여넣기 복원 |
교훈
- Named attribute 사용:
size.lines,size.columns처럼 이름으로 접근하면 positional unpacking의 순서 혼동을 방지할 수 있다. - 전체 비활성화 vs 선택적 필터링: protocol 전체를 끄면 부작용이 생긴다. 문제의 원인(CapsLock keycode)만 정확히 제거하는 것이 올바른 접근이다.
- 터미널 프로토콜 수준의 문제: 앱 업그레이드 시 예고 없이 발생할 수 있으므로, wrapper를 통한 방어적 접근이 유용하다.
- Cmd+V vs Ctrl+V: macOS 터미널 앱에서 Cmd 단축키는 앱(iTerm2)이 가로채고, Ctrl 단축키만 터미널 프로세스에 전달된다. 이미지 붙여넣기 등 앱 내 기능은 Ctrl+V를 사용해야 한다.