라인 모드 프롬프트에 2,000자 붙여넣기 했더니 macOS가 여전히 1,024자로 제한하는 문제의 원인
요약
macOS 터미널에서 긴 명령어를 실행할 때 발생하는 1,024바이트 제한 문제의 원인을 분석했습니다. 이는 XNU 커널 레벨에서 정의된 `MAX_CANON` 및 `MAX_INPUT` 상수로 인해 발생하며, 시스템 설계와 관련된 문제입니다.
핵심 포인트
- macOS 터미널은 XNU 커널에 의해 1,024바이트로 입력 길이가 제한됨.
- 이 제한은 `bsd/sys/syslimits.h` 파일의 상수 정의에서 기인함.
- 터미널 입출력 처리는 라인 디시플린(line discipline)과 캐노니컬 모드에 의존함.
- 리눅스는 macOS와 달리 4,095바이트로 더 큰 버퍼 크기를 가짐.
- 도서: System Design Pocket Guide: Fundamentals
- 시리즈: Pocket Guides for Developers — 에이전트, RAG, 프롬프트, 관측 가능성(observability), 이벤트, 데이터베이스 및 시스템 설계에 대한 8권의 짧은 책들 — 전체 시리즈
- 나의 프로젝트: Hermes IDE | GitHub — Claude Code 및 기타 AI 코딩 도구와 함께 배포하는 개발자를 위한 IDE
- 나: xgabriel.com | GitHub
2026년 7월, 누군가가 Claude Code에 대해 issue #81485를 제출했습니다: macOS에서 터미널에 실행 명령을 입력하여 세션을 여는 딥 링크가 해당 명령이 1,024바이트보다 길 경우 아무 말 없이 실패하는 문제가 있었습니다. 보고서에는
터미널 창은 커널 장치 쌍인 가상 터미널(pseudo-terminal)을 통해 프로그램과 통신합니다. 터미널 앱이 입력한 키 스트로크를 한쪽 끝에 쓰고, 프로그램이 다른 쪽에서 읽어들이며, 그 사이에 라인 디시플린(line discipline)이 위치합니다. 기본적으로 이는 캐노니컬 모드(canonical mode)로 작동하는데, 리눅스 termios(3) man page에 명확히 설명되어 있습니다: "입력은 라인 단위로 제공됩니다." 또한 라인 편집(지우기, 삭제) 기능이 활성화됩니다.
그렇기 때문에 cat 명령어는 편집 코드가 없음에도 불구하고 Backspace가 작동합니다. 커널이 자체 버퍼에 해당 라인을 보관하고, 그곳에서 Backspace와 Ctrl-U를 적용한 후, 사용자가 Enter 키를 누를 때만 read() 함수로 라인을 전달합니다. 그때까지 프로그램은 아무것도 인식하지 못합니다.
stty를 사용하여 모드를 확인할 수 있습니다:
stty -a | grep -o -- '-\{0,1\}icanon'
icanon은 캐노니컬 모드가 켜져 있음을 의미합니다. -icanon은 꺼져 있음을 의미하며, 매뉴얼 페이지에 나열된 stty sane 명령을 실행하면 이를 다시 켜줍니다.
커널의 라인 버퍼는 크기가 고정되어 있습니다. 잠시만 주목해 주세요.
macOS에서는 1,024 바이트, 리눅스에서는 4,095 바이트
macOS에서 이 제한은 XNU의 bsd/sys/syslimits.h에 존재합니다:
#define MAX_CANON 1024 /* max bytes in term canon input line */
#define MAX_INPUT 1024 /* max bytes in terminal input */
이것을 강제하는 코드는 XNU의 bsd/kern/tty.c에 있는 ttyinput()입니다. 큐에 쌓인 입력이 MAX_INPUT에 도달하면, 새로운 문자는 버려지고, IMAXBEL로 설정된 경우 커널은 벨을 울립니다. 시그널 키(Ctrl-C), 플로우 제어, 지우기 및 삭제 키는 오버플로우 검사 이전에 처리됩니다. 줄 바꿈(newline)과 EOF 키는 그 이후에 처리됩니다. 따라서 라인이 가득 차면 Ctrl-C와 Backspace는 여전히 작동하지만, Enter와 Ctrl-D는 다른 문자처럼 손실됩니다. Claude Code 명령어는 Enter가 도착하지 않았기 때문에 절대 실행되지 않았습니다.
2024년의 Apple Developer Forums thread에서는 사용자 측면에서도 동일한 사례를 보여줍니다: base64를 실행하고 붙여넣기(paste)를 하니, "1024자(바이트)까지 붙여넣거나 입력할 수 있습니다. 그 이후로는 터미널이 비프음(beeping)을 내며 더 이상 타이핑이나 붙여넣기를 허용하지 않습니다."라고 합니다. Apple의 DTS 엔지니어는 이 제한이 "터미널 앱(terminal.app) 자체가 아니라 셸과 근본적인 tty가 I/O를 관리하는 방식에서 비롯된다"고 답변했습니다.
Linux는 더 긴 라인을 허용하며 오버플로우를 다르게 처리합니다. termios(3)에 따르면 최대 라인은 "4096자(종료되는 개행 문자 포함)"이며, "4095자를 초과하여 (하지만 종료되는 개행 문자를 제외한) 모든 입력 데이터는 폐기됩니다."라고 명시되어 있습니다. 그 뒤의 버퍼는 N_TTY_BUF_SIZE이며, 이는 drivers/tty/n_tty.c에서 4096으로 정의됩니다. Linux는 개행 문자를 유지하므로 잘린 라인도 제출됩니다. macOS는 이를 버리므로, 그 라인은 제자리에 남아 있게 됩니다.
두 운영체제 모두 프로그램에게 알려주지 않습니다. 프로그램은 더 짧은 라인을 받고 그것을 알 방법이 없습니다.
직접 테스트해보기
개행 문자(newline)가 없는 5,000자 길이의 라인을 클립보드에 넣어보세요. macOS에서는:
head -c 5000 /dev/zero | tr '\0' a | pbcopy
Linux에서는 동일한 head | tr를 데스크톱이 사용하는 어떤 클립보드 도구로 파이프(pipe)하세요. 이제 wc -c를 실행하고, 붙여넣기한 후 Enter를 누르고, Ctrl-D를 누르세요:
wc -c
위의 출처들을 바탕으로 예상되는 결과는 다음과 같습니다. Linux에서는 termios(3)에 따라 4,095자에 개행 문자를 더해 총 4096자가 유지되므로, wc는 5001이 아닌 4096을 출력해야 합니다. macOS에서는 1,024바이트 이후 벨 소리가 울리고, Enter와 Ctrl-D는 모두 폐기되기 때문에 아무런 동작도 하지 않습니다. 빠져나오려면 Ctrl-C를 누르세요.
이제 캐노니컬 모드(canonical mode)가 꺼진 상태에서 동일한 붙여넣기를 하나의 명령어로 해보세요:
stty -icanon min 1; head -c 5000 | wc -c; stty icanon
-icanon은 라인 어셈블리(line assembly)를 비활성화하고, min 1(stty 매뉴얼 페이지에 따르면, "-icanon을 사용하면 완료된 읽기 작업에 대해 최소 N 문자를 설정합니다")는 바이트가 도착하는 즉시 각read()호출이 반환되도록 합니다.head는 바이트가 도착함에 따라 이를 소진하므로, 어떤 라인도 커널의 버퍼에 맞출 필요가 없습니다. 만약 TTY가 문제였다면, 이 명령어는 5000을 보고할 것입니다. 해당 포럼 스레드의 Apple 엔지니어는 pico에 매우 긴 단일 라인을 아무 문제 없이 붙여넣는다고 설명합니다. pico`와 같은 전체 화면 편집기는 비정규화 모드(non-canonical mode)로 읽기 때문에, 라인 버퍼가 가득 차지 않습니다.
아무도 읽지 않을 때, 쓰기 작업자가 기다린다
줄 잘림 현상(Truncation)은 데이터를 읽는 프로그램이 필요합니다. 다른 실패 사례는 그렇지 않은 경우입니다: 당신이 수천 줄의 스택 트레이스(stack trace)를 붙여넣는 동안 컴파일 중이거나, 잠들었거나, 멈춘 프로세스가 필요한 경우입니다.
바이트들은 여전히 어딘가로 가야 합니다. 리눅스에서는 라인 규율(line discipline)의 4,096바이트 읽기 버퍼에 도달하며, 이 버퍼가 가득 차면 라인 규율은 데이터 수신을 중단하고 바이트들은 pty의 플립 버퍼(flip buffers)에 쌓입니다. Linux pty 드라이버에는 반대편에 남은 공간이 얼마나 되는지 보고하는 pty_write_room() 함수가 있으며, 그 뒤의 플립 버퍼는 tty_buffer.c에서 정의된 바이트 제한인 TTYB_DEFAULT_MEM_LIMIT를 가집니다. 공간이 부족해지면, 터미널 앱 측에서의 쓰기 작업은 n_tty_write()를 통해 진행되며, 이는 두 가지 중 하나를 수행합니다. 파일 디스크립터가 비차단(non-blocking)인 경우 -EAGAIN을 반환하고, 차단(blocking)인 경우 리더가 공간을 확보할 때까지 wait_woken()에서 잠듭니다.
따라서 터미널 앱은 다시는 읽지 않을 수도 있는 프로그램에 대해 기다리게 됩니다. 만약 그 쓰기 작업이 나머지 앱이 필요로 하는 스레드에서 실행되거나, 크기 조정 및 닫기 경로(resize and close paths)가 사용하는 잠금(lock)을 보유하고 있다면, 이 멈춤 현상은 한 탭에서 전체 창으로 확산됩니다. 모든 터미널 호스트는 자체적으로 붙여넣기 데이터를 큐에 넣고 PTY가 소진됨에 따라 이를 공급해야 하며, 절대 이에 대해 차단되어서는 안 됩니다.
저는 Hermes IDE를 만들었으니 이 부분을 염두에 두고 읽어주세요. 2026년 10월 12일의 2.2.9 릴리스에서 Hermes의 이러한 종류의 버그가 수정되었습니다. 해당 노트에는 다음과 같이 적혀 있습니다: "프로그램이 읽지 않는 터미널에 많은 양의 텍스트를 붙여넣는 것이 더 이상 창 크기 조정, 분할 또는 종료 시 전체 창을 멈추게 하지 않습니다." 이 노트는 패치를 설명하지 않으므로 내부 작동 방식은 추측하지 않겠습니다. 즉, 프로그램이 읽기를 멈춘 터미널에 대용량 로그를 붙여넣은 후, 해당 창(pane)의 크기 조정, 분할 또는 종료가 발생하는 경우를 이 노트가 설명하는 상황입니다.
에이전트 TUI가 Raw 모드로 전환하는 이유
대화형 CLI는 코딩 에이전트를 포함하여 커널이 입력 내용을 편집하는 것을 원하지 않습니다. 그들은 키가 눌리는 순간마다 모든 키 입력을 받고 싶어 하며, 이를 통해 자체 프롬프트를 그리고, 화살표 키를 처리하며, 붙여넣은 블록을 접을 수 있는데, 이것이 바로 Raw 모드가 제공하는 기능입니다. Node에서 setRawMode(true)는 "입력이 항상 문자 단위로 사용 가능하다" 및 "터미널에 의한 입력 문자의 모든 특수 처리가 비활성화되며, 여기에는 입력 문자의 에코잉도 포함된다"를 의미합니다. 심지어 Ctrl-C가 SIGINT 전송을 멈추게 합니다.
Claude Code의 대화형 UI는 이런 방식으로 구축되었습니다. 2025년 5월 이슈에는 파이프된 입력으로 실행했을 때 발생한 오류가 나와 있습니다: "Raw 모드는 현재 process.stdin에서 지원되지 않습니다. Ink는 기본적으로 이를 입력 스트림으로 사용합니다." Ink는 터미널을 위한 React 렌더러이며, setRawMode가 stdin이 이를 수행할 수 없을 때 오류를 발생시킵니다.
Raw 모드는 1,024바이트의 벽을 수정합니다. 프로그램이 바이트를 수신하는 대로 읽고 라인을 자체 메모리에 유지하기 때문입니다. 이는 새로운 문제를 만듭니다. 붙여넣기(paste)가 매우 빠르게 타이핑된 것처럼 보이게 됩니다. 붙여넣기 내의 개행 문자(newline)는 사용자가 Enter 키를 누른 것처럼 보이게 합니다. 이것이 바로 브래킷 패스트(bracketed paste)의 목적입니다. 프로그램은 ESC [ ? 2004 h를 전송하며, 그 이후부터는 xterm control sequences에 따라 터미널이 모든 붙여넣기를 ESC [ 200 ~와 ESC [ 201 ~로 감싸서 "프로그램이 붙여넣은 텍스트와 직접 입력한 텍스트를 구별할 수 있도록" 만듭니다. Ink의 usePaste 후크가 이를 자동으로 활성화하며, GNU Readline에는 기본값이 "On"인 enable-bracketed-paste 기능이 있습니다.
한 가지 주의할 점은 끝 마커(end marker)가 붙여넣기 텍스트 내부에 몰래 삽입될 수 있다는 것입니다. Ghostty paste docs는 \x1b[201~를 포함하는 데이터를 안전하지 않다고 간주하는데, 이는 "브래킷 패스트 모드를 종료하고 명령을 주입하는 데 사용될 수 있기" 때문입니다. 웹 페이지에서 생성된 명령어를 에이전트의 프롬프트에 복사하는 행위 역시 터미널에 붙여넣는 과정입니다.
이를 어떻게 처리할 것인가
긴 텍스트를 붙여넣었는데 내용이 짧거나 전송되지 않는 경우, 애플리케이션을 비난하기 전에 상대방 프로그램이 데이터를 어떻게 읽는지 문의하십시오. 일반적인 캐노니컬 모드(canonical mode)의 read() 함수가 주된 용의자로 의심됩니다.
- 자신의 스크립트로 긴 입력을 처리할 때는 파이프나 파일을 사용하십시오:
pbpaste | ./tool또는./tool < trace.txt. 파이프와 파일은 라인 규율(line discipline)이 없습니다. - 붙여넣기 입력을 받는 CLI를 작성한다면, 비캐노니컬 모드(non-canonical mode)로 읽거나 그렇게 하는 라인 편집기를 사용하고 브래킷 패스트를 활성화하십시오.
- 사용자에게 터미널에 타이핑하는 모든 것을 작성할 때는, macOS에서는 각 라인을 1,024바이트 미만으로 유지하거나 페이로드(payload)를 파일 또는 환경 변수를 통해 전달하십시오.
- 터미널 호스트를 구축한다면, PTY로의 쓰기 작업을 영원히 대기할 수 있는 것으로 처리해야 합니다.
1,024는 커널 헤더에 있는 #define 값 중 하나입니다. 그럼에도 불구하고 이 숫자는 에이전트가 전체 스택 트레이스(stack trace)를 받을지 여부를 결정합니다.
이 글이 유용했다면
이 게시물의 절반은 제약 버퍼(bounded buffer)가 제 역할을 하는 것입니다. 소비자가 멈추면 생산자는 기다리거나 데이터를 버려야 하며, 선택할 수 있는 설계 결정은 어느 쪽인지뿐입니다. 데이터가 어디서 대기하고, 그곳에서 문제가 생겼을 때 무슨 일이 일어나는지에 대해 생각하는 것을 좋아한다면, 이는 부하 상태에서도 계속 작동해야 하는 시스템의 핵심 구성 요소인 _System Design Pocket Guide: Fundamentals_에서 다루는 내용입니다. 이 책은 에이전트(agents), RAG, 프롬프트(prompts), 관측 가능성(observability), 이벤트(events), 데이터베이스(databases) 및 시스템 설계에 대한 8권의 짧은 책 중 하나이며, 전체 시리즈는 여기에서 확인할 수 있습니다. 또한 Claude Code와 다른 에이전트들을 나란히 실행한다면, 무료이며 소스 코드가 공개된 Hermes IDE를 GitHub에서 사용할 수 있습니다.

AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기