[기술검토] claude -p 헤드리스 자동화에 PTY 도입 검토 — 버퍼링 이슈와 Agent SDK Plan 정책 대응, 3-LLM 교차검증
PTY 개념·파이프와의 차이·asyncio 통합 패턴부터, stdout 만 PTY 로 바꾸는 PTY-lite PoC 설계, '-p 없이 PTY 인터랙티브 자동화'로 구독 풀 차감을 노리는 시나리오의 정책/ToS/운영 리스크 4축 분석, codex·gemini 5라운드 교차검증까지 정리한 기술검토.
[기술검토] 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) 로 자식 프로세스를 띄우는 방식 도입을 검토하게 됨:
- 일부 호출에서
MISSING result event가 관측됨 (워커의 진단 로그). stdout 이 pipe 일 때 CLI 가 block-buffered 로 동작하면서 마지막 result 이벤트가 잘려나가는 케이스 가설. - 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/ptmx를open(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() | False | True |
| 자식 출력 버퍼링 (일반적) | block-buffered (느림) | line-buffered (실시간) — 단 프로그램이 setvbuf 등으로 자체 설정하면 예외 |
| ANSI 컬러/escape | 보통 비활성 | 활성 — raw bytes 로 섞임 |
| TUI / spinner | 안 그려짐 | 동적으로 다 그려짐 |
| keypress 전송 | 못 함 | master_fd 에 1바이트 쓰면 됨 |
| Ctrl-C | 직접 signal | PTY 입력으로 \x03 byte 송신 또는 PGID 에 SIGINT |
| 윈도우 크기 | 없음 | TIOCSWINSZ 로 통보 |
| 종료 감지 | EOF on pipe | EOF (macOS) / EIO (Linux) |
| stderr 분리 | 별도 fd | stdout 과 합쳐서 한 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 시 발생하는 EIO 가 protocol.connection_lost 의 예외로 전달되므로 그 분기를 별도로 처리해야 함.
💡 정확성 보강 (codex/gemini 교차검증): "
add_reader사용 전에O_NONBLOCK반드시 선행" 같은 단정은 부정확 — 설계에 따라 다르다. 또한 "connect_read_pipe는 EIO 때문에 비표준" 같은 단정도 부정확하다. 둘 다 공식 지원되며, 종료/EOF/EIO 처리만 구현에서 신경 쓰면 된다. 우리 PoC 는 EIO 분기를 명시적으로 제어할 수 있다는 이점 때문에 (A) 를 선택.
이게 _run_process_streaming 의 proc.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 가 해결할 가능성이 있는 문제
- 버퍼링 지연 — 워커의
proc.stdout.read(256KB chunk)에서 stream-json 라인 도착이 지연되는 케이스 - MISSING result event — pipe block-buffering 으로 마지막 이벤트 누락 가설
- TUI 진행 표시 — spinner / "thinking" / tool 진행률 캡처
옵션 C 의 비용
- ANSI escape 가 stdout 에 섞임 —
\x1b[2K\r같은 게 stream-json 라인 사이에 끼면 JSON parse 깨짐. strip 필터 필수. - EOF 처리가 까다로움 — Linux 에서는 master_fd read 가
OSError(errno=EIO)로 떨어지는 게 일반적이고, macOS/BSD 에서는 0-byte read (EOF) 가 일반적이다. OS/커널/상황별 차이가 있을 수 있으므로 둘 다 처리하는 패턴이 안전. (교차검증: codex / gemini 모두 동의 — codex 는 "macOS=항상 0-byte" 같은 단정 표현은 과함을 경고) - 터미널 폭에 따른 wrap —
TIOCSWINSZ로 매우 넓게(예: 1000 cols) 잡지 않으면 stream-json 한 줄이 두 줄로 갈라짐. - stream-json + isatty 거동 미실측 — claude CLI 가 isatty 일 때 stream-json 모드에서 spinner/색 추가를 끼워 넣을 가능성. 실험 필요. (또한 일반적으로
isatty()==True면 stdio 가 line-buffered 가 되지만, 프로그램이 자체적으로 setvbuf 등으로 버퍼링을 바꾸는 케이스도 존재 — "PTY=항상 line-buffered" 로 단정하지 말 것) - 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) 대응 가능성 검토
정책 요지
- 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 //resumepicker 등 UI 인터랙션도 자동 응답해야 함.
축 2. 백엔드의 인터랙티브 vs Agent SDK 식별 메커니즘
- ❓ 공식 비공개 (3-LLM 교차검증 합의). Anthropic 공식 문서로 확인되는 것은 "분리 정책 존재" 까지이며, 실제 식별 키 (argv / 헤더 / 엔드포인트 / 토큰 타입 / TTY 판별 로직) 와 우선순위는 공개되어 있지 않다.
- 단, 공식 문서에서 확인 가능한 강한 신호: 로그인 방식 (구독 OAuth vs
ANTHROPIC_API_KEY) 이 1차 분기에 깊이 연결됨.ANTHROPIC_API_KEY가 있으면 구독이 아니라 API 과금으로 간다고 명시. → 워커의billing_mode토글 (subscriptionvsapi_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 정책의 회피 수단으로 부적합하다.
근거:
- 식별 메커니즘이 공식 비공개 → "PTY 면 구독 풀로 분류된다" 는 가정이 작동할지 자체가 실측 없이는 불가
- Consumer ToS / AUP 의 자동화 접근·우회 금지 조항과 해석상 충돌 소지가 큼 → 계정 제한 / 청구 재분류 / 차단 리스크
- 탐지·집행 체계 공식 존재 → 자동화 패턴이 누적되면 abuse 로 재분류될 가능성
- 자동화 신뢰성 측면에서도 stream-json 없는 raw text 파싱 + UI 인터랙션 자동 응답은 critical path (24/7 자동개발 파이프라인 등) 에 부적합
단정 회피: "기술적으로 즉시 작동하지 않는다" 와 "정책상 허용되지 않는다" 는 별개. 전자는 실험으로 확인 가능, 후자는 정책 해석 리스크가 영구히 남는다.
도입 결정 전에 반드시 측정해야 할 항목 (4개)
만약 그래도 시도한다면, 본격 도입 전 다음을 측정하고 문서화:
- 실제 과금 경로 측정 — 동일 작업을 (a) interactive PTY 자동화 vs (b)
-pvs (c) SDK 로 각각 실행했을 때 청구 명세에서 실제 차감 풀 / 한도 카운터의 차이 - 이상 신호 모니터링 — 응답 코드 (429, 401, 403) / 경고 메일 / 계정 알림 수신 여부
- 장시간 안정성 — 24시간 / 72시간 자동화 시 세션 차단 / rate-limit 발생 시점
- 정책 문의 문서화 — Anthropic 공식 지원 채널 (support@anthropic.com) 로 사용 시나리오 허용 여부를 문서로 받아두기. 이게 가장 확실한 리스크 해소책.
PTY 도입의 정당한 동기 (정책과 무관)
위 시나리오와 별개로, PTY 도입이 정책과 무관하게 정당한 동기 3개:
- stream-json 출력 실시간성 — MISSING result event / pipe block-buffering 으로 인한 progress 지연 완화
- 다른 CLI 의 인터랙티브 프롬프트 자동 응답 — codex / gemini 등
--dangerously-skip-permissions등가 옵션이 없는 CLI 를pexpect패턴으로 자동 응답 - 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 vsANTHROPIC_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-fast | N초 응답 없으면 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.json의permissions.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 가 우리한테 안 맞는다는 신호:
- stream-json 한 줄 무결성 —
WORKER_PTY_STDOUT=1모드에서 result 이벤트 캡처 성공률이 기존과 동일한지 ("result event captured" 로그 카운트). 줄 wrap 이 발생하면 JSON parse 실패 → MISSING 증가. - session_id 조기 echo —
_post_early_session_id가 PTY 경로에서도 동일 시점에 발사되는지. backendskill_runs.cli_session_idNULL 비율이 증가하면 안 됨. - stderr 분리 보존 —
result.stderr가 비어있지 않게 (기존에 진단 메시지 받던 케이스 한정). - 종료/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 (정정 후)
- stream-json + isatty 조합의 거동 미실측 — claude CLI 가 isatty=True 일 때 stream-json 모드에서 spinner/progress 를 추가로 끼워 넣을지 모름. 만약 그렇다면 ANSI strip 만으론 부족하고, "JSON 으로 파싱 가능한 줄만 통과" 패턴이 더 안전 (현재
_process_line의json.JSONDecodeError: return이 이미 그렇게 동작). - 고아 프로세스 정리 로직 — 프로세스 이름 기반 PID 매칭은 PTY 경로에서도 동일하게 동작해야 하지만, controlling terminal 차이로 ps 출력의
tty컬럼이 달라질 가능성. 매칭 로직이 tty 안 보면 무관. - 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 한 결과:
| # | 클레임 | codex | gemini | 본문 반영 |
|---|---|---|---|---|
| 1 | claude -p = --print 의 non-interactive/headless 모드 | ✅ 정확 | ✅ 정확 | 그대로 |
| 2 | PTY EOF: Linux=EIO / macOS=0-byte read | ⚠️ "macOS=항상 0-byte" 단정은 과함 | ✅ 정확 | "일반적으로" 로 표현 완화 |
| 3 | pipe=block-buffered / PTY=line-buffered | ✅ 정확 (caveat: 프로그램 자체 버퍼링 변경 가능) | ✅ 정확 | "일반적으로" + setvbuf caveat 추가 |
| 4 | 과금 분기 = CLI argv 기준, TTY/PIPE 무관 | ⚠️ "TTY/PIPE 절대 영향 0" 공식 근거 없음 | ✅ 정확 | "공식 분기 기준에 TTY/PIPE 등장 안 함" 으로 보수화 |
| 5 | asyncio + 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 자가 평가 편향을 가질 수 있다는 우려 → 메타 오퍼레이터 워크플로우 도입.
워크플로우
- 동일한 정책 질문 3개 (Q1 식별 메커니즘, Q2 ToS 위치, Q3 봇 탐지) 를 Codex + Gemini + Claude 각각에 답변하게 함
- Codex 를 메타 오퍼레이터로 다시 호출 → 3개 답변을 입력으로 주고 종합 결론 / 일치·불일치 / hallucination 식별 / 본문 반영 권고 도출
정책 클레임 결과표
| Q | 질문 | Claude | Gemini | Codex (1차) | 메타 오퍼레이터 종합 |
|---|---|---|---|---|---|
| 1 | 인터랙티브 vs Agent SDK 식별 메커니즘 | ❓ 비공개 + (a) argv 추정 | ⚠️ "모드 식별자 헤더 전송" 단정 (VentureBeat 출처) | ❓ 비공개 + API key 유무는 분기 신호 명시 | ❓ 공식적으로 비공개. 식별 키 / 우선순위 단정 불가. 단 ANTHROPIC_API_KEY 유무는 공식 분기 신호로 확인됨. |
| 2 | PTY 인터랙티브 자동화 = 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 도메인 우선)
- Claude Help Center (Agent SDK credit 분기): https://support.claude.com/en/articles/15036540-use-the-claude-agent-sdk-with-your-claude-plan
- Claude Help Center (Pro/Max plan): https://support.claude.com/en/articles/11145838-use-claude-code-with-your-pro-or-max-plan
- Claude Help Center (Using agents per usage policy): https://support.claude.com/en/articles/12005017-using-agents-according-to-our-usage-policy
- Anthropic Consumer ToS: https://www.anthropic.com/legal/consumer-terms
- Anthropic AUP: https://www.anthropic.com/legal/aup
- Anthropic Transparency Hub (탐지·집행 체계 1차 근거): https://www.anthropic.com/transparency/system-trust-reporting
- Anthropic Threat Intelligence Report (2025-03, 대화 패턴 분석 + 계정 ban 사례): https://www.anthropic.com/news/detecting-and-countering-malicious-uses-of-claude-march-2025
- Anthropic Rate Limits (한도 / 429 동작, 별도 트래픽 제어 근거 — 탐지·집행 근거로는 약함): https://docs.anthropic.com/en/api/rate-limits
- Anthropic CLI reference: https://docs.anthropic.com/en/docs/claude-code/cli-usage
- Python asyncio event loop: https://docs.python.org/3/library/asyncio-eventloop.html
- ptyprocess: https://ptyprocess.readthedocs.io/
💡 출처 정책: 본 문서의 단정 범위는 위 공식 도메인 (
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 토글 + 모델/스킬 튜닝 + 크레딧 모니터링 |
후속 액션 제안
- PTY-lite PoC 실측 —
--output-format stream-json --verbose가 PTY 환경에서 한 줄 = 한 JSON 을 유지하는지 검증 (1일) - 1번이 OK 면 옵션 C 코드 머지 +
WORKER_PTY_STDOUT환경변수로 staged rollout - 1번이 깨지면 옵션 C 폐기 — 우리 stream-json 의존 구조에서 PTY 의 비용 > 이득 결론
댓글
아직 댓글이 없어요. 첫 댓글을 남겨보세요!
★이 강의 후기
후기 남기기아직 이 강의에 대한 후기가 없어요.