증상

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.02026-03-26kitty keyboard protocol 전체 비활성화 (ESC[>1u 제거)
v1.0.12026-03-26PTY 크기 전달 버그 수정 — ioctl TIOCGWINSZ 기반으로 교체
v1.0.22026-03-28SIGWINCH 핸들러 안정화, set_winsize() ioctl 통합
v1.1.02026-03-30kitty protocol 유지, CapsLock 57358u만 필터링 → Ctrl+V 이미지 붙여넣기 복원

교훈

  1. Named attribute 사용: size.lines, size.columns처럼 이름으로 접근하면 positional unpacking의 순서 혼동을 방지할 수 있다.
  2. 전체 비활성화 vs 선택적 필터링: protocol 전체를 끄면 부작용이 생긴다. 문제의 원인(CapsLock keycode)만 정확히 제거하는 것이 올바른 접근이다.
  3. 터미널 프로토콜 수준의 문제: 앱 업그레이드 시 예고 없이 발생할 수 있으므로, wrapper를 통한 방어적 접근이 유용하다.
  4. Cmd+V vs Ctrl+V: macOS 터미널 앱에서 Cmd 단축키는 앱(iTerm2)이 가로채고, Ctrl 단축키만 터미널 프로세스에 전달된다. 이미지 붙여넣기 등 앱 내 기능은 Ctrl+V를 사용해야 한다.