코디움랩
강의자료 목록
Materials2026년 7월 3일무료0

[기술검토] claude -p 헤드리스 자동화에 PTY 도입 검토 — 버퍼링 이슈와 Agent SDK Plan 정책 대응, 3-LLM 교차검증

PTY 개념·파이프와의 차이·asyncio 통합 패턴부터, stdout 만 PTY 로 바꾸는 PTY-lite PoC 설계, '-p 없이 PTY 인터랙티브 자동화'로 구독 풀 차감을 노리는 시나리오의 정책/ToS/운영 리스크 4축 분석, codex·gemini 5라운드 교차검증까지 정리한 기술검토.

#PTY#pseudo-terminal#claude-code#headless#asyncio#stream-json#pexpect#ptyprocess#Agent-SDK#구독정책#ToS#AUP#교차검증#fact-check#PoC

[기술검토] claude -p 헤드리스 자동화에 PTY 도입 검토 — 버퍼링 이슈와 구독 정책 대응, 3-LLM 교차검증

이 문서는 claude -p 헤드리스 자동화 파이프라인에 PTY(Pseudo-Terminal)를 도입하는 안을 검토한다. 결론: stdout 만 PTY 로 바꾸는 "PTY-lite" PoC 는 버퍼링 완화 목적에 한해 유효하지만, "-p 를 안 쓰고 PTY 인터랙티브 자동화로 구독 풀 차감을 노리는" 시나리오는 정책·운영 양면에서 부적합하다 (3-LLM 메타 검증 합의).

배경

자체 구축한 AI 개발 파이프라인(멀티에이전트 하네스)의 자동화는 헤드리스 실행 워커(이하 워커)가 claude -p <prompt> --dangerously-skip-permissions --output-format stream-json --verbose 형태로 스킬/커맨드를 헤드리스 호출하고, stdout 의 stream-json 라인을 WebSocket 으로 중계하는 구조다 (워커의 _run_process_streaming 함수).

최근 두 가지 동기로 PTY (Pseudo-Terminal) 로 자식 프로세스를 띄우는 방식 도입을 검토하게 됨:

  1. 일부 호출에서 MISSING result event 가 관측됨 (워커의 진단 로그). stdout 이 pipe 일 때 CLI 가 block-buffered 로 동작하면서 마지막 result 이벤트가 잘려나가는 케이스 가설.
  2. 2026-06-15 부 Anthropic 의 정책 변경 대응 — 인터랙티브 Claude Code 는 구독 한도, claude -p 헤드리스는 별도 Agent SDK 크레딧 한도로 분리. -p 자체를 안 쓰고 PTY 자동화로 인터랙티브 분류 / 구독 풀에서 차감되도록 시도 하려는 동기. (자세한 분석은 §6)

이 문서는 PTY 의 동작 원리부터 우리 시스템에 어떻게 녹일 수 있는지, 그리고 위 두 동기가 실제로 PTY 도입의 합당한 근거가 되는지까지를 정리한다.


1. claude -p 는 헤드리스 모드가 맞다 — 확인

-p (--print) 는 공식적으로 "non-interactive / one-shot" 모드:

  • stdin/stdout/stderr 이 파이프 에 붙어 자식 입장에서 isatty()False
  • TTY 가 없으니 spinner / alternate screen / raw key input 비활성
  • stdout 이 파이프일 때 CLI 는 보통 block-buffered 로 전환 → 한 번에 큰 청크로 출력이 쏟아질 수 있음
  • SIGWINCH 같은 터미널 시그널 미수신

워커의 asyncio.create_subprocess_exec(..., stdout=PIPE, stderr=PIPE, ...) 호출이 정확히 이 헤드리스 전제 위에 짜여 있다.


2. PTY (Pseudo-Terminal) 가 뭔가

개념

PTY 는 커널이 제공하는 가상 터미널 (master/slave) 페어 추상화이며, 파이프와 유사한 바이트 스트림 통신을 양방향으로 제공한다. 정확히는 pipe(2) 가 아니라 pseudo-terminal 메커니즘이다:

  • 부모가 /dev/ptmxopen(2) 하면 마스터 fd (익명) 를 반환받고, 동시에 슬레이브 측 디바이스 (/dev/pts/N) 가 자동 생성됨

  • /dev/ptmx 자체는 "마스터 fd 를 얻는 인터페이스" 이며 마스터 디바이스 그 자체가 아님

  • 사용 모델 측면에서만 "양방향 파이프 쌍" 처럼 다뤄진다:

  • Master (PTM, controller): 부모 프로세스가 쥐는 쪽. 쓰면 자식의 키보드 입력으로, 읽으면 자식의 터미널 출력으로 보임.

  • Slave (PTS, controlled): 자식의 stdin/stdout/stderr 에 붙는 쪽. 자식 입장에선 /dev/pts/N 같은 진짜 터미널 디바이스.

핵심은 자식이 isatty() == True 라고 믿는다는 점. line discipline, ECHO, ICANON, signal generation 같은 게 다 동작.

파이프와의 결정적 차이

항목PIPE (현재)PTY
isatty()FalseTrue
자식 출력 버퍼링 (일반적)block-buffered (느림)line-buffered (실시간) — 단 프로그램이 setvbuf 등으로 자체 설정하면 예외
ANSI 컬러/escape보통 비활성활성 — raw bytes 로 섞임
TUI / spinner안 그려짐동적으로 다 그려짐
keypress 전송못 함master_fd 에 1바이트 쓰면 됨
Ctrl-C직접 signalPTY 입력으로 \x03 byte 송신 또는 PGID 에 SIGINT
윈도우 크기없음TIOCSWINSZ 로 통보
종료 감지EOF on pipeEOF (macOS) / EIO (Linux)
stderr 분리별도 fdstdout 과 합쳐서 한 fd (옵션 C 는 별도 파이프로 분리)

마지막 stderr 합쳐짐이 우리에게 큰 변수.


3. Python 에서 PTY 쓰는 법

세 가지 레벨:

(a) pty 표준 라이브러리 — 저수준

pty.openpty()os.fork()os.dup2(slave_fd, 0/1/2)os.execvp. fork/select/SIGCHLD/EOF/리사이즈/cleanup 다 직접. asyncio 와 자연스레 안 묶임.

(b) ptyprocess — pexpect 의 PTY 코어

from ptyprocess import PtyProcess
p = PtyProcess.spawn(["claude", "-p", "/스킬", "--dangerously-skip-permissions"],
                     dimensions=(40, 200))
chunk = p.read(4096)

API 는 단순하나 동기 → asyncio 에 넣으려면 loop.add_reader 직접 등록.

(c) pexpect — 자동화 친화 고수준

import pexpect
child = pexpect.spawn("claude -p ...", encoding="utf-8", timeout=600)
child.expect(["Continue? \(y/N\)", pexpect.EOF])
child.sendline("y")

인터랙티브 프롬프트(y/N, 비밀번호) 자동 응답에 최적. 우리는 --dangerously-skip-permissions 로 그런 게 없으니 오버킬.

asyncio 통합 패턴 (우리 케이스에 가장 중요)

asyncio 에는 PTY 전용 API 가 없음. 두 가지 공식 지원 경로:

(A) loop.add_reader 기반 (저수준, 권장) — PTY 특유의 종료 시그널(EIO on Linux)을 직접 제어하기에 가장 직관적:

master_fd, slave_fd = pty.openpty()
proc = await asyncio.create_subprocess_exec(
    *cmd,
    stdin=slave_fd, stdout=slave_fd, stderr=slave_fd,
    cwd=cwd, start_new_session=True, env=sub_env,
)
os.close(slave_fd)
# master_fd 를 non-blocking 으로 두고 add_reader 콜백 등록
loop.add_reader(master_fd, on_readable)

(B) loop.connect_read_pipe 기반 (고수준)SelectorEventLoop 가 내부적으로 fd 를 non-blocking 으로 설정해주는 공식 지원 경로. 단, slave close 시 발생하는 EIOprotocol.connection_lost 의 예외로 전달되므로 그 분기를 별도로 처리해야 함.

💡 정확성 보강 (codex/gemini 교차검증): "add_reader 사용 전에 O_NONBLOCK 반드시 선행" 같은 단정은 부정확 — 설계에 따라 다르다. 또한 "connect_read_pipe 는 EIO 때문에 비표준" 같은 단정도 부정확하다. 둘 다 공식 지원되며, 종료/EOF/EIO 처리만 구현에서 신경 쓰면 된다. 우리 PoC 는 EIO 분기를 명시적으로 제어할 수 있다는 이점 때문에 (A) 를 선택.

이게 _run_process_streamingproc.stdout.read(...) 자리를 갈아끼우는 모양이 된다.


4. 우리 시스템에 녹이는 3가지 옵션

옵션 A — 풀 PTY 교체 (비추)

모든 cli_agent 경로를 PTY 로 옮김. stderr 합쳐짐 / ANSI 섞임 / EOF 처리 / winsize 등 5가지 비용을 다 떠안음.

옵션 B — 하이브리드 (조건부 권장)

기존 stream-json 경로는 그대로, output_format=text + 인터랙티브 확인이 필요한 CLI (codex / gemini 등) 한정으로 PTY 경로로 분기. pexpect-style 자동 응답까지 확장 가능.

옵션 C — PTY-lite (PoC 권장)

stdout 만 PTY 로 띄우고 stdin 은 DEVNULL, stderr 은 PIPE 유지. 자식의 isatty(1) == True 효과(line-buffer, 컬러) 만 누리고, stderr 분리는 그대로 유지하며, 구조 변경을 최소화하는 절충점.

옵션 C 가 해결할 가능성이 있는 문제

  1. 버퍼링 지연 — 워커의 proc.stdout.read(256KB chunk) 에서 stream-json 라인 도착이 지연되는 케이스
  2. MISSING result event — pipe block-buffering 으로 마지막 이벤트 누락 가설
  3. TUI 진행 표시 — spinner / "thinking" / tool 진행률 캡처

옵션 C 의 비용

  1. ANSI escape 가 stdout 에 섞임\x1b[2K\r 같은 게 stream-json 라인 사이에 끼면 JSON parse 깨짐. strip 필터 필수.
  2. EOF 처리가 까다로움 — Linux 에서는 master_fd read 가 OSError(errno=EIO) 로 떨어지는 게 일반적이고, macOS/BSD 에서는 0-byte read (EOF) 가 일반적이다. OS/커널/상황별 차이가 있을 수 있으므로 둘 다 처리하는 패턴이 안전. (교차검증: codex / gemini 모두 동의 — codex 는 "macOS=항상 0-byte" 같은 단정 표현은 과함을 경고)
  3. 터미널 폭에 따른 wrapTIOCSWINSZ 로 매우 넓게(예: 1000 cols) 잡지 않으면 stream-json 한 줄이 두 줄로 갈라짐.
  4. stream-json + isatty 거동 미실측 — claude CLI 가 isatty 일 때 stream-json 모드에서 spinner/색 추가를 끼워 넣을 가능성. 실험 필요. (또한 일반적으로 isatty()==True 면 stdio 가 line-buffered 가 되지만, 프로그램이 자체적으로 setvbuf 등으로 버퍼링을 바꾸는 케이스도 존재 — "PTY=항상 line-buffered" 로 단정하지 말 것)
  5. kill/cleanup — 워커의 고아 프로세스 정리 로직(PGID 시그널)이 PTY controlling-terminal 차이로도 일관되게 전파되는지 재검증.

5. 옵션 C PoC 설계

변경 지점 4곳 (실제 코드 수정은 별도 진행):

1. unix-only 모듈 lazy import (워커 스크립트 상단)

try:
    import errno
    import fcntl
    import pty
    import struct
    import termios
    _PTY_AVAILABLE = True
except ImportError:
    _PTY_AVAILABLE = False

2. 헬퍼 + 신규 함수 (_run_process_streaming 직전)

ANSI_ESCAPE_RE = re.compile(
    rb'\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07]*\x07|\x1b[=>]|\r'
)

def _set_winsize(fd: int, rows: int = 40, cols: int = 1000) -> None:
    """cols 가 좁으면 CLI 가 stream-json 한 줄을 wrap 함 → 1000 cols 로 강제."""
    try:
        fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack("HHHH", rows, cols, 0, 0))
    except OSError:
        pass

async def _run_process_streaming_pty(cmd, cwd=None, timeout=None, ...):
    """_run_process_streaming 의 PTY-stdout 변형 (옵션 C).
    - stdout 만 PTY slave fd
    - stderr 는 그대로 PIPE
    - stdin 은 DEVNULL
    - master_fd 를 non-blocking + loop.add_reader 로 asyncio 통합
    - ANSI escape strip
    - Linux EOF (EIO) / macOS EOF (0-byte) 둘 다 처리
    """
    master_fd, slave_fd = pty.openpty()
    _set_winsize(master_fd, rows=40, cols=1000)
    flags = fcntl.fcntl(master_fd, fcntl.F_GETFL)
    fcntl.fcntl(master_fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)

    proc = await asyncio.create_subprocess_exec(
        *cmd,
        stdin=subprocess.DEVNULL,
        stdout=slave_fd,
        stderr=asyncio.subprocess.PIPE,
        cwd=cwd, start_new_session=True, env=sub_env,
    )
    os.close(slave_fd)
    # ... add_reader + queue + ANSI strip + line split + _process_line(text)
    # 기존 _run_process_streaming 의 _process_line / _send_progress /
    # _post_early_session_id / heartbeat 로직은 거의 그대로 재사용

3. opt-in 게이트 (_run_process_streaming 본문 첫 줄)

async def _run_process_streaming(cmd, cwd=None, ...):
    if (_PTY_AVAILABLE
        and os.environ.get("WORKER_PTY_STDOUT", "").lower() in ("1", "true", "yes")):
        return await _run_process_streaming_pty(cmd, cwd=cwd, ...)
    # ─── 이하 기존 본문 그대로 ───

4. 운영 토글

WORKER_PTY_STDOUT=1 make up

총 변경량: 신규 ~200줄 + 기존 1줄 수정.


6. ⭐ Claude Agent SDK Plan 정책(2026-06-15) 대응 가능성 검토

정책 요지

Anthropic 공식 안내:

  • 2026-06-15 부 Pro/Max/Team/Enterprise 구독자에게 월별 Agent SDK 크레딧 별도 지급 (출처: 공식 안내, 2026-05-23 시점 확인):
    • Pro: $20
    • Max 5x: $100
    • Max 20x: $200
    • Team Standard seats: $20
    • Team Premium seats: $100
    • Enterprise usage-based: $20
    • Enterprise seat-based Premium seats: $200
    • (Enterprise Standard seats 는 대상 아님 — 공식 안내 각주)
  • 인터랙티브 Claude Code (터미널 / IDE) → 구독 한도 차감
  • claude -p (헤드리스), Agent SDK, GitHub Actions, 3rd-party Agent SDK 앱 → Agent SDK 크레딧 차감
  • 크레딧 소진 시: 사용량 크레딧 (pay-as-you-go) 활성화 필요 또는 다음 월까지 대기
  • API key 사용자는 Agent SDK 크레딧 미지급 (기존 pay-as-you-go 유지)

PTY 도입의 실제 동기 (정정)

⚠️ 본 섹션은 1차 작성본에서 동기 가정이 잘못되어 전면 재작성됨. 1차 작성본은 "PTY 로 claude -p 를 감싸도 -p argv 가 결정하니 회피 불가" 라는 결론으로 흘러갔으나, 실제 동기는 "-p 자체를 안 쓰려고" 였다.

자동화 파이프라인의 실제 도입 동기는:

claude-p 없이 인터랙티브 REPL 모드로 띄우고, PTY (pexpect 패턴) 로 프롬프트를 자동 입력 / 응답을 raw text 로 파싱 → 구독 한도 풀에서 차감되도록 시도

공식 안내는 인터랙티브 Claude Code 를 구독 한도, -p / Agent SDK 를 별도 크레딧 풀로 분리한다고 명시했으므로, 이론적으로 "인터랙티브로 보이게 자동화하면 월정액에 포함" 이라는 발상이 가능하다.

이 발상이 실제로 작동할지 — 4축 분석

축 1. 기술적 가능성

  • 가능. pexpect / ptyprocess / 또는 PTY 위 raw I/O 로 인터랙티브 REPL 의 프롬프트 자동 입력 + 응답 파싱은 기술적으로 구현 가능.
  • 단, 인터랙티브 모드는 stream-json 같은 구조화 출력이 없어서 raw text 정규식 파싱 / 화면 escape sequence 처리가 필요하다. spinner / model selector / permission prompt / /resume picker 등 UI 인터랙션도 자동 응답해야 함.

축 2. 백엔드의 인터랙티브 vs Agent SDK 식별 메커니즘

  • ❓ 공식 비공개 (3-LLM 교차검증 합의). Anthropic 공식 문서로 확인되는 것은 "분리 정책 존재" 까지이며, 실제 식별 키 (argv / 헤더 / 엔드포인트 / 토큰 타입 / TTY 판별 로직) 와 우선순위는 공개되어 있지 않다.
  • 단, 공식 문서에서 확인 가능한 강한 신호: 로그인 방식 (구독 OAuth vs ANTHROPIC_API_KEY) 이 1차 분기에 깊이 연결됨. ANTHROPIC_API_KEY 가 있으면 구독이 아니라 API 과금으로 간다고 명시. → 워커의 billing_mode 토글 (subscription vs api_key) 과 직결되는 부분.
  • 단정 불가: "PTY 가 인터랙티브로 인식되어 구독 풀에 들어간다" 도, "들어가지 않는다" 도 공식 근거로 단정할 수 없다.

축 3. ToS / AUP 정책 위치

  • ⚠️ 위반 리스크 높음 (3-LLM 교차검증 합의).
  • Consumer ToS자동화 / 비인간 수단 (bot / script 등) 접근 금지직접 조항으로 명시되어 있다 (API key 등 명시 허용 상황 외). 본 시나리오 (PTY 자동화) 가 충돌하는 1차 근거.
  • AUP 는 주로 "남용 / 가드레일 우회 / 스팸성 자동화" 맥락 — 위 1차 조항에 추가 리스크를 강화하는 보조 근거로 봐야 함 (AUP 만으로 단정하는 건 과함).
  • 다만 "PTY 자동화" 를 콕 집어 직접 금지하는 문구 까지는 공식 문서에서 확인되지 않음 — 정책 의도 / 우회 의도 / 해석에 따라 위반으로 재분류될 가능성.
  • 출처: Anthropic Consumer ToS, Anthropic AUP.

축 4. 봇 탐지 / 패턴 분석 리스크

  • 탐지 / 집행 체계 존재 공식 확인 (3-LLM 교차검증 합의), 세부 규칙 비공개.
  • Anthropic Transparency Hub 에서 Safeguards 팀의 "detections and monitoring 기반 정책 집행 (경고 / 제한 / 종료)" 명시.
  • 2025-03 위협 인텔 리포트 에서 "대화 패턴 분석 / 분류기로 오남용 탐지 및 계정 ban 사례" 제시.
  • "Pro / Max 한도는 인간 사용 패턴 기준" 같은 정확한 공식 문구는 확인되지 않음 (gemini 가 단정한 "Human usage patterns" 라는 용어는 hallucination 의심 — codex 가 명시적으로 반박).
  • 출처: Anthropic Transparency Hub, Rate Limits Docs.

최종 권고 (3-LLM 메타 합의)

PTY 기반 인터랙티브 자동화는 Plan 정책의 회피 수단으로 부적합하다.

근거:

  1. 식별 메커니즘이 공식 비공개 → "PTY 면 구독 풀로 분류된다" 는 가정이 작동할지 자체가 실측 없이는 불가
  2. Consumer ToS / AUP 의 자동화 접근·우회 금지 조항과 해석상 충돌 소지가 큼 → 계정 제한 / 청구 재분류 / 차단 리스크
  3. 탐지·집행 체계 공식 존재 → 자동화 패턴이 누적되면 abuse 로 재분류될 가능성
  4. 자동화 신뢰성 측면에서도 stream-json 없는 raw text 파싱 + UI 인터랙션 자동 응답은 critical path (24/7 자동개발 파이프라인 등) 에 부적합

단정 회피: "기술적으로 즉시 작동하지 않는다" 와 "정책상 허용되지 않는다" 는 별개. 전자는 실험으로 확인 가능, 후자는 정책 해석 리스크가 영구히 남는다.

도입 결정 전에 반드시 측정해야 할 항목 (4개)

만약 그래도 시도한다면, 본격 도입 전 다음을 측정하고 문서화:

  1. 실제 과금 경로 측정 — 동일 작업을 (a) interactive PTY 자동화 vs (b) -p vs (c) SDK 로 각각 실행했을 때 청구 명세에서 실제 차감 풀 / 한도 카운터의 차이
  2. 이상 신호 모니터링 — 응답 코드 (429, 401, 403) / 경고 메일 / 계정 알림 수신 여부
  3. 장시간 안정성 — 24시간 / 72시간 자동화 시 세션 차단 / rate-limit 발생 시점
  4. 정책 문의 문서화 — Anthropic 공식 지원 채널 (support@anthropic.com) 로 사용 시나리오 허용 여부를 문서로 받아두기. 이게 가장 확실한 리스크 해소책.

PTY 도입의 정당한 동기 (정책과 무관)

위 시나리오와 별개로, PTY 도입이 정책과 무관하게 정당한 동기 3개:

  1. stream-json 출력 실시간성 — MISSING result event / pipe block-buffering 으로 인한 progress 지연 완화
  2. 다른 CLI 의 인터랙티브 프롬프트 자동 응답 — codex / gemini 등 --dangerously-skip-permissions 등가 옵션이 없는 CLI 를 pexpect 패턴으로 자동 응답
  3. TUI-only 정보 캡처 — spinner / tool 진행률 등 stream-json 외 채널 정보 보존

옵션 C PoC 의 위치

옵션 C (stdout 만 PTY) PoC 는 위 3개 정당한 동기 중 첫번째 에만 한정된다. claude -p 를 그대로 유지하면서 stdout 의 line-buffering 효과만 누리는 설계 → 정책 분류에 영향 없음, ToS 리스크 없음. 본 문서 §5 의 PoC 설계는 그 의도에서 작성된 것이며, 본 §6 의 "인터랙티브 자동화 시나리오" 와는 다른 도입 경로임을 명시.

Plan 정책 대응의 권장 경로 (PTY 무관)

PTY 가 아니라 다음 경로들이 대응책:

  • 별도 기술검토 "claude -p 비용 정책 변경(2026-06-15) 대응" 문서가 다루는 5단계 + PoC 7건 + 모드 토글 등을 적용
  • billing_mode 토글 — 구독 OAuth vs ANTHROPIC_API_KEY 주입 분기 (이미 구현). 공식 문서에서 가장 명확하게 분기 시그널로 확인된 부분.
  • 멀티 CLI 지원 (cli_agent ∈ claude/codex/gemini) — 특정 벤더 의존도 분산 (이미 구현)
  • 모델 튜닝 — 비용 민감 단계는 Haiku, critical 단계만 Sonnet/Opus
  • 단계별 스킬튜닝 / 결과 후처리 캐싱
  • 대시보드 비상등 (크레딧 소진 임박 알림)

7. PTY 만 썼을 때 사용자 확인 요청이 들어오면 — 자동화는 멈춘다

§6 의 시나리오 ("-p 없이 인터랙티브 REPL 을 PTY 로 띄움") 가 작동할지에 대한 가장 즉각적인 운영 질문: PTY 로 자식만 띄우고 prompt 자동 응답 매커니즘이 없으면, claude CLI 가 사용자 확인을 띄우는 순간 자동화는 어떻게 되나?

답: 대부분의 확인 prompt 는 응답이 없으면 자동화가 그 자리에서 정지한다 (hang) — 영구히 또는 timeout 까지. (CLI 구현에 따라 즉시 에러 종료 / 자동 거부 처리가 적용되는 일부 prompt 도 존재할 수 있으나, 권한 / 확인성 prompt 의 일반적 동작은 stdin 대기 → hang.)

이건 옵션 C PoC 와는 무관한 이야기다 — 옵션 C 는 -p 헤드리스 + --dangerously-skip-permissions 라서 prompt 자체가 발생하지 않음. 본 §7 은 §6 의 시나리오 한정 분석.

7.1 PTY 만 띄웠을 때 prompt 가 발생하면 실제로 어떻게 작동하는가

OS 레벨 동작 시퀀스:

부모가 명시적으로 timeout 을 안 걸어두면 자식은 stdin read 에서 영원히 블록된다. 부모도 master_fd read 가 더 이상 새 데이터를 받지 않으므로 (자식이 아무것도 안 보냄) idle 상태로 대기.

: PTY 는 그냥 "TTY 인 척하는 통로" 일 뿐, prompt 를 자동 응답하는 기능은 없다. 응답을 하려면 부모가 master_fd 에 명시적으로 write 해야 한다.

7.2 그러면 어떻게 처리하는가 — 4가지 옵션

옵션방식자동화 무중단?부담
(a) 자동 응답pexpect 패턴 매칭 → 자동 sendline✅ (패턴 매칭이 맞을 때만)
(b) 사람 개입prompt 감지 시 알림 → 사람이 응답❌ (대기)중간
(c) Timeout + fail-fastN초 응답 없으면 kill + 재시도❌ (해당 run 실패)작음
(d) 사전 우회prompt 발생 자체를 줄이는 설정⚠️ (일부만)작음

(a) 자동 응답 — pexpect 패턴 매칭

import pexpect
child = pexpect.spawn("claude", encoding="utf-8", timeout=600,
                     dimensions=(40, 200))
patterns = [
    r"Allow Bash command.*\(y/n",       # 0: permission
    r"Trust this folder",                # 1: trust
    r"Select a model:",                  # 2: model selector
    r"Resume which session\?",           # 3: resume picker
    r"Compact now",                      # 4: auto-compact
    r"^> $",                             # 5: REPL prompt (응답 도착)
    pexpect.EOF,                         # 6
    pexpect.TIMEOUT,                     # 7
]
while True:
    idx = child.expect(patterns)
    if idx in (0, 1, 4):
        child.sendline("y")
    elif idx == 2:
        child.sendline("\n")     # default
    elif idx == 3:
        child.sendline("1")      # 첫 세션
    elif idx == 5:
        # 응답 추출 / 다음 prompt 송신
        ...
    elif idx in (6, 7):
        break

한계 (왜 부담이 큰가):

  • CLI 버전 업그레이드 시 prompt 문구가 바뀌면 정규식 miss → timeout hang
  • 새 종류 prompt 가 Anthropic 측에 추가되면 패턴에 없어서 hang
  • ANSI escape / 박스 문자 (╭─╮│└─┘) / 컬러 코드가 prompt 텍스트 사이에 끼면 정규식 깨짐 → ANSI strip 필터 일관성 필요
  • 응답 본문 파싱 자체가 fragile — stream-json 같은 명확한 event boundary 없음, markdown rendered 출력에서 "응답 끝 / 다음 prompt 시작" 경계 모호

(b) 사람 개입 — alert + 수동 응답

부모가 prompt 감지 시 메신저 / 이메일 / 대시보드 알림 → 사람이 master_fd 에 응답 write. 자동화의 의미가 약화되지만 "예외 상황만 사람" 패턴으로 활용 가능. critical path 가 아닌 단발성 자동화에 적합.

(c) Timeout + fail-fast

응답 없는 prompt 발생 시 N초 후 자식 kill + 해당 run 실패 처리 → 24/7 자동개발 같은 critical path 에서 시스템 hang 만은 방지. 다만 그 run 자체는 실패 처리되므로 retry 로직 / 백오프 / 사람 알림이 또 필요.

# 단순 구조 — master_fd read 에 timeout
data_q = asyncio.Queue()
try:
    item = await asyncio.wait_for(data_q.get(), timeout=PROMPT_TIMEOUT)
except asyncio.TimeoutError:
    # 응답 없음 → 사용자 확인 대기 중일 가능성
    proc.kill()
    raise PromptStuckError("prompt detected but no auto-responder configured")

(d) 사전 우회 — prompt 발생 자체를 줄이는 설정

  • ~/.claude/settings.jsonpermissions.allow 배열 (CLI 플래그가 아닌 설정 파일 키) 에 자주 쓰는 tool (Bash/Read/Write/Edit) pre-approve → permission prompt 발생 빈도 감소
  • --permission-mode bypassPermissions → permission prompt 일괄 우회. : 보안 의미가 --dangerously-skip-permissions 와 사실상 동일 → 이럴 거면 헤드리스 -p 가 낫다는 모순 발생
  • --model <name> 명시 → model selector prompt 줄이기
  • --continue 또는 명시적 session id → resume picker 우회
  • Trust folder pre-config (CLI 버전마다 존재 여부 확인)

위 1~5 를 다 적용해도 모든 prompt 가 사라지지는 않는다 — 새 안전 prompt / API error retry / auto-compact / 신규 prompt 추가 등은 여전히 발생.

7.3 처리 옵션의 결정 매트릭스

시나리오권장 처리
단발성 실험 / PoC(b) 사람 개입 + 알림 — 가장 간단, 부담 최소
정기 배치 (소수 실행)(d) 사전 우회 + (c) timeout fail-fast — 운영 부담 작고 hang 방지
Critical path (24/7 자동개발 등)이 시나리오는 권장 안 함 — (a) 자동 응답이 필요한데 fragility 너무 큼, 헤드리스 -p 가 정답

7.4 24/7 자동개발 같은 critical path 에 대한 함의

24/7 + 이슈 트래커의 이슈 단위로 동작하는 자동개발 파이프라인에서:

  • 한 번의 pattern miss / 신규 prompt 미대응이 발생하면 전체 이슈 자동개발이 timeout 까지 정체 → SLA 영향, 이슈 처리 지연
  • pattern miss 가 재발하면 fallback / kill / 재시작 로직 필요 — 워커의 기존 PID 탐색 / 스킬 취소 로직과 별개의 prompt-stuck 복구 경로 추가
  • 사람 개입 없이 24/7 도는 게 핵심 가치인데, prompt 하나에 멈춰버리면 그 가치 무너짐

헤드리스 -p + stream-json 의 핵심 강점이 정확히 "이런 부담 없음" 인데, 정책 회피 목적으로 이 부담을 다시 떠안는 건 ROI 가 마이너스. §6 의 결론 (PTY 인터랙티브 자동화는 정책 회피 수단으로 부적합) 이 정책 / ToS 측면뿐 아니라 운영 측면에서도 동일하게 도출되는 이유.

7.5 한 줄 요약

⚠️ PTY 만 띄우고 prompt 자동 응답 없으면 → claude 의 첫 확인 요청에서 자동화는 멈춘다. 처리하려면 (a) pexpect 자동 응답 (부담 큼) / (b) 사람 개입 (자동화 의미 약화) / (c) timeout fail-fast (안전하나 진행 못 함) / (d) 사전 우회 (부분적) 중 선택. 모두 헤드리스 -p 보다 부담이 크고 신뢰성이 낮다.


8. 옵션 C PoC 검증 항목

옵션 C PoC 적용 시 다음 4가지를 동시 측정 — 하나라도 깨지면 옵션 C 가 우리한테 안 맞는다는 신호:

  1. stream-json 한 줄 무결성WORKER_PTY_STDOUT=1 모드에서 result 이벤트 캡처 성공률이 기존과 동일한지 ("result event captured" 로그 카운트). 줄 wrap 이 발생하면 JSON parse 실패 → MISSING 증가.
  2. session_id 조기 echo_post_early_session_id 가 PTY 경로에서도 동일 시점에 발사되는지. backend skill_runs.cli_session_id NULL 비율이 증가하면 안 됨.
  3. stderr 분리 보존result.stderr 가 비어있지 않게 (기존에 진단 메시지 받던 케이스 한정).
  4. 종료/cleanup_active_procs 에서 빠지는지, 스킬 취소 로직이 PTY 자식까지 죽이는지. start_new_session=True + PTY controlling-terminal 관계의 PGID 시그널 실측 필요.

측정 방법

for i in $(seq 1 10); do
  curl -X POST "$BACKEND/skills/<id>/execute" -d '{"input":{"issue_key":"TEST-1"}}'
done

# 비교 지표:
# - MISSING result event 비율 (워커 로그 grep)
# - skill_runs.cli_session_id NULL 비율 (DB)
# - 첫 progress 이벤트까지 평균 latency
# - 평균 total duration_ms

9. 알려진 리스크 / Known Unknowns (정정 후)

  1. stream-json + isatty 조합의 거동 미실측 — claude CLI 가 isatty=True 일 때 stream-json 모드에서 spinner/progress 를 추가로 끼워 넣을지 모름. 만약 그렇다면 ANSI strip 만으론 부족하고, "JSON 으로 파싱 가능한 줄만 통과" 패턴이 더 안전 (현재 _process_linejson.JSONDecodeError: return 이 이미 그렇게 동작).
  2. 고아 프로세스 정리 로직 — 프로세스 이름 기반 PID 매칭은 PTY 경로에서도 동일하게 동작해야 하지만, controlling terminal 차이로 ps 출력의 tty 컬럼이 달라질 가능성. 매칭 로직이 tty 안 보면 무관.
  3. Docker 환경에서의 PTY 권한/dev/ptmx 권한 확인 필요. 현재 워커는 host 에서 도므로 무관하지만, 향후 컨테이너화 시 점검 포인트.

10. 교차검증 (3-LLM 메타 분석)

10.1 기술 클레임 5개 fact-check (1차 라운드)

본 문서의 PTY 관련 기술 클레임 5개를 OpenAI Codex (gpt-5.3-codex) 와 Google Gemini CLI 로 병렬 fact-check 한 결과:

#클레임codexgemini본문 반영
1claude -p = --print 의 non-interactive/headless 모드✅ 정확✅ 정확그대로
2PTY EOF: Linux=EIO / macOS=0-byte read⚠️ "macOS=항상 0-byte" 단정은 과함✅ 정확"일반적으로" 로 표현 완화
3pipe=block-buffered / PTY=line-buffered✅ 정확 (caveat: 프로그램 자체 버퍼링 변경 가능)✅ 정확"일반적으로" + setvbuf caveat 추가
4과금 분기 = CLI argv 기준, TTY/PIPE 무관⚠️ "TTY/PIPE 절대 영향 0" 공식 근거 없음✅ 정확"공식 분기 기준에 TTY/PIPE 등장 안 함" 으로 보수화
5asyncio + PTY = O_NONBLOCK + add_reader 표준⚠️ O_NONBLOCK 반드시 선행 X, connect_read_pipe 도 공식 지원✅ 정확두 경로 모두 공식 지원 명시, EIO 처리 이점으로 (A) 선택 근거화

10.2 정책 클레임 3개 fact-check + 메타 오퍼레이터 종합 (2차 라운드)

문제 인식: 1차 라운드는 기술 클레임만 다뤘고, 정작 본 문서의 핵심 동기 (§6 의 "-p 를 안 쓰고 PTY 자동화로 구독 풀 차감 시도") 의 정책 평가가 누락되어 있었음. 또한 Claude (본 문서 작성 LLM) 가 Anthropic 자가 평가 편향을 가질 수 있다는 우려 → 메타 오퍼레이터 워크플로우 도입.

워크플로우

  1. 동일한 정책 질문 3개 (Q1 식별 메커니즘, Q2 ToS 위치, Q3 봇 탐지) 를 Codex + Gemini + Claude 각각에 답변하게 함
  2. Codex 를 메타 오퍼레이터로 다시 호출 → 3개 답변을 입력으로 주고 종합 결론 / 일치·불일치 / hallucination 식별 / 본문 반영 권고 도출

정책 클레임 결과표

Q질문ClaudeGeminiCodex (1차)메타 오퍼레이터 종합
1인터랙티브 vs Agent SDK 식별 메커니즘❓ 비공개 + (a) argv 추정⚠️ "모드 식별자 헤더 전송" 단정 (VentureBeat 출처)❓ 비공개 + API key 유무는 분기 신호 명시❓ 공식적으로 비공개. 식별 키 / 우선순위 단정 불가. 단 ANTHROPIC_API_KEY 유무는 공식 분기 신호로 확인됨.
2PTY 인터랙티브 자동화 = ToS 위반?⚠️ 명시 금지 부재 가능, 의도 위반 가능✅ "기술적 제한 우회 명시 금지" (Reddit/VentureBeat 출처)⚠️ Consumer ToS / AUP 자동화 접근·우회 금지 조항과 충돌 소지⚠️ 위반 리스크 높음. ToS / AUP 의 자동화 접근·우회 금지 조항과 해석상 충돌. "PTY 자동화 콕 집어 금지" 문구는 확인 안 됨.
3봇 탐지 / 자동화 패턴 탐지 공식 명시❓ 공식 명시 부재✅ "Human usage patterns" 용어 직접 사용 단정 (PopAI 출처)✅ Transparency Hub / 위협 인텔 리포트로 탐지 체계 명시 + "Human usage patterns" 정확 문구는 ❌ 없음✅ 탐지·집행 체계 공식 확인. 세부 룰 비공개. "Human usage patterns" 같은 정확 문구는 공식 문서에 없음 (Gemini hallucination 의심).

Hallucination / 신뢰도 낮은 출처 식별 (메타 오퍼레이터 판정)

Gemini 답변에서 다음은 검증 불가 또는 신뢰도 낮음으로 분류:

  • "Human usage patterns" 라는 Anthropic 공식 용어 단정 → 공식 문서에서 확인 불가
  • "밀리초 단위 프롬프트 전송 / 사고 정지 시간 없는 기계적 요청 탐지" 구체 시그널 단정 → 공식 1차 출처 없음
  • VentureBeat, PopAI, Reddit thread 출처 → 검증 불가
  • "PTY 자동화 명시적 금지" 처럼 읽히는 표현 → ToS/AUP 일반 조항의 해석이지 명시는 아님

메타 오퍼레이터 최종 권고 (§6 에 반영됨)

PTY 기반 인터랙티브 자동화는 Plan 정책의 회피 수단으로 부적합. 근거: 식별 메커니즘 비공개 + ToS/AUP 충돌 소지 + 탐지·집행 체계 존재 + 자동화 신뢰성 한계. 도입 결정 4개 실측 항목 필수 (§6 마지막 참고).

10.3 출처 (공식 Anthropic 도메인 우선)

💡 출처 정책: 본 문서의 단정 범위는 위 공식 도메인 (anthropic.com / support.claude.com / docs.anthropic.com / code.claude.com) 의 확인 가능한 문구로 제한한다. 비공식 출처 (블로그 / 커뮤니티) 기반 주장은 "검증 불가" 로 별도 표기되었다.

⚠️ URL 시점 caveat: 위 URL 들은 2026-05-23 시점 접근 가능 / WebFetch 검증 완료. 정책 시행 시점 (2026-06-15) 이후 콘텐츠가 변경되거나 archived 될 수 있다.

10.4 전체 본문 fact-check (3차 라운드)

v4 본문 전체 (~570 라인) 를 codex 에 한 번 더 던져서 남아 있는 사실 오류 / 부정확한 단정 / 코드 참조 어긋남 을 점검 — 11개 발견 사항 + 4개 정확 OK 식별. 주요 정정 (v4 → v5):

카테고리원본정정
PTY 정의"양방향 파이프 한 쌍""가상 터미널 master/slave 장치 쌍, 파이프 유사 바이트 스트림"
버퍼링 단정"PIPE block / PTY line-buffered""일반적 경향, setvbuf 등으로 예외 가능" caveat 추가
Ctrl-C 처리"tcsendbreak / 직접 키 byte""PTY 입력으로 \x03 byte 또는 PGID 에 SIGINT"
정책 표현"AUP 자동화 접근·우회 금지""Consumer ToS 직접 조항 + AUP 는 보조 강화"
코드 참조워커 스크립트의 라인 번호 9곳 불일치실제 코드 위치로 전면 정정
함수명고아 정리 함수명 오기실제 함수명으로 정정
§7 hang 단정"영구히 정지""대부분 hang, 일부는 즉시 에러 종료 가능"
§10 서브섹션9.1~9.4 (번호 불일치)10.1~10.4 정렬
permissions.allow표현 불분명"설정 파일 키 (CLI 플래그 아님)" 명시

10.5 전체 본문 fact-check — gemini 라운드 (4차)

v5 본문 전체를 gemini 에 던져 추가 점검. gemini 의 발견을 codex 의 v4 결과와 비교 + 공식 출처로 재검증한 결과:

Gemini 의 유효한 발견 (반영됨, v5 → v6)

발견본문 위치정정
/dev/ptmx 가 "마스터 디바이스 그 자체" 처럼 읽힘§3 개념"/dev/ptmx = 마스터 fd 를 얻는 인터페이스, open 시 익명 마스터 fd + /dev/pts/N 슬레이브 자동 생성" 으로 정확화
출처 URL 시점 명시 부재§10.3 출처 정책"2026-05-23 시점 WebFetch 검증 완료, 정책 시행 후 변경 가능" caveat 추가
Enterprise 크레딧 세분화 누락§6 정책 요지"usage-based $20 / seat-based Premium $200" 분리 + Enterprise Standard seats 미대상 각주 추가 (공식 안내 재확인)

Gemini 의 기각된 발견 (공식 출처 / 도구 검증으로 반박)

주장반박 근거
"크레딧 금액 $20/$100/$200 은 공식 확정 안 됐을 가능성 (hallucination 의심)"WebFetch 재확인 시 공식 안내 페이지에 정확히 명시 (Pro $20 / Max 5x $100 / Max 20x $200 등). 표 그대로 일치.
"Support URL 15036540 은 존재하지 않을 가능성"2026-05-23 시점 WebFetch 성공, 콘텐츠 정상.
"--permission-mode bypassPermissions 는 공식 옵션 리스트에 없음"로컬 claude --help 출력에 공식 명시: --permission-mode <mode> (choices: "acceptEdits", "auto", "bypassPermissions", "default", "dontAsk", "plan")
"permissions.allow 가 아니라 allowedTools 가 settings.json 키"공식 Settings 페이지permissions.allow / permissions.deny 배열 구조 명시. 본문 표현 정확.

Gemini 의 부분 반영 발견

발견반영
"라인 번호는 코드 변경 시 어긋나므로 함수명 참조 권장"본문은 함수명 참조로 정리. 코드 위치는 작성 시점 (2026-05-23) 기준.
"'부적합' 톤 과함 → '리스크 매우 높음 / 도입 권장 어려움' 권장"본문은 이미 §10.2 / §6 결론에서 "단정 회피" 명시. "부적합" 자체는 3-LLM 메타 합의의 결론으로 유지 — 추가 완화는 메시지 약화 우려로 미반영.

메타 관찰

Gemini 는 v5 의 5개 항목을 hallucination 의심으로 지적했는데, 4개가 공식 출처 / 도구 검증으로 반박됨. 즉 gemini 본인이 hallucination 의심하는 패턴 이 더 두드러짐 — 1차 라운드 (§10.2 의 "Human usage patterns" 단정) 와 동일한 패턴의 재현. 본 라운드의 정직한 결론: gemini 가 단정한 것은 항상 1차 출처로 재검증해야 한다. (codex 는 v4 → v5 에서 11개 정정 중 11개 모두 유효였던 것과 대조)

10.6 검증 메서드 재현 명령

# 1차: 기술 클레임 fact-check
cat /tmp/factcheck_prompt.txt | codex exec --skip-git-repo-check -
gemini -p "$(cat /tmp/factcheck_prompt.txt)"

# 2차: 정책 클레임 fact-check
cat /tmp/factcheck_prompt_v2.txt | codex exec --skip-git-repo-check -
gemini -p "$(cat /tmp/factcheck_prompt_v2.txt)"

# 3차: 메타 오퍼레이터 종합 (codex 가 3개 답변을 입력으로 받아 종합)
cat /tmp/operator_final_prompt.txt | codex exec --skip-git-repo-check -

# 4차: codex 전체 본문 fact-check (v4 → v5 정정 추출)
cat /tmp/fullbody_final_prompt.txt | codex exec --skip-git-repo-check -

# 5차: gemini 전체 본문 fact-check (v5 검증, 유효/기각 분류)
gemini -p "$(cat /tmp/fullbody_final_prompt_v5.txt)"

핵심 정정 포인트는 모두 codex / 메타 오퍼레이터의 보수적 표현을 채택했다.


11. 결론

  • 현재 -p + stream-json 조합은 헤드리스 자동화의 정답에 가까운 모드. PTY 도입의 주된 이득은 "버퍼링 완화 / 실시간성" 정도.
  • 도입한다면 옵션 C (PTY-lite, stdout 만 PTY) 가 구조 변경 최소화 측면에서 최선. PoC 변경량 ~200줄 + 1줄.
  • 도입 전 검증 1개: claude -p ... --output-format stream-json --verbose 를 PTY 안에서 돌려 stream-json 한 줄이 깨지지 않는지 확인하는 것이 결정 포인트.
  • 2026-06-15 Plan 정책 대응으로 "-p 안 쓰고 PTY 인터랙티브 자동화" 시도는 부적합 (3-LLM 메타 합의, §6). 근거: 식별 메커니즘 공식 비공개 + Consumer ToS/AUP 자동화 접근·우회 금지 조항과 해석상 충돌 + 탐지·집행 체계 공식 존재 + 자동화 신뢰성 한계. 다만 "절대 작동 안 함" 같은 단정은 공식 근거 부족 이라 시도 자체를 막을 사실은 없음 — 그래서 도입 4개 실측 (실제 차감 풀 확인 / 이상신호 모니터링 / 장시간 안정성 / Anthropic 공식 채널 문의) 이 필수.
  • 정책 대응의 권장 경로: PTY 가 아니라 멀티 CLI (cli_agent) / billing_mode 토글 (subscription ↔ api_key, 공식 분기 신호로 확인됨) / 모델 튜닝 / 스킬튜닝 / 대시보드 비상등 경로 (§6 마지막 참고).

요약

질문결론
PTY 도입의 정당한 이득은?stream-json 버퍼링 완화 / 실시간성 — 그 이상은 아님
어떤 방식으로 도입?옵션 C (PTY-lite): stdout 만 PTY, stderr 는 PIPE 유지, env 토글 opt-in
결정 포인트는?PTY 안에서 stream-json 한 줄 = 한 JSON 유지 여부 실측
"-p 없이 PTY 인터랙티브 자동화" 로 구독 풀 차감?부적합 — 식별 메커니즘 비공개 + ToS/AUP 충돌 소지 + 탐지 체계 존재 + 운영 신뢰성 낮음
정책 대응의 권장 경로는?멀티 CLI + billing_mode 토글 + 모델/스킬 튜닝 + 크레딧 모니터링

후속 액션 제안

  1. PTY-lite PoC 실측 — --output-format stream-json --verbose 가 PTY 환경에서 한 줄 = 한 JSON 을 유지하는지 검증 (1일)
  2. 1번이 OK 면 옵션 C 코드 머지 + WORKER_PTY_STDOUT 환경변수로 staged rollout
  3. 1번이 깨지면 옵션 C 폐기 — 우리 stream-json 의존 구조에서 PTY 의 비용 > 이득 결론

댓글

0/2000

아직 댓글이 없어요. 첫 댓글을 남겨보세요!

이 강의 후기

후기 남기기

아직 이 강의에 대한 후기가 없어요.