claude -p 체이닝 6단계: Phase 2가 죽는 구조적 이유와 V3.5 우회 패턴
📚 AI 직원 회사 빌드기 — 4부작 시리즈
-
③ claude -p 6-step 체이닝 — V3.5 + Phase 2 트랩 (현재 글)
-
④ 30시간 회고 — 토큰·막힌 5번·다음 6주 KPI (다음 주 발행 예정)
36분짜리 자동화가 Phase 2에서 멈춘다
claude -p로 블로그 파이프라인을 체이닝하면 특정 단계에서 subprocess가 멈추고 stdout이 비어 돌아온다. returncode는 0이다. 에러 메시지도 없다. 타임아웃까지 그냥 블로킹된다.
이게 Phase 2 트랩이다. blog-draft 스킬이 프레임·제목 후보를 제안하고 “진행 시 Phase 3 시작”이라는 텍스트를 출력한 뒤, 사용자 승인을 기다린다. subprocess.run() 안에서 승인을 기다리면 타임아웃까지 블로킹되고, 결국 md 파일이 생성되지 않은 채 다음 단계로 못 넘어간다.
이 글은 V3 5-step 파이프라인이 V3.5 6-step으로 바뀐 설계 기록이다. 트랩이 왜 발생하는지 구조적으로 분석하고, 실제 코드 변경 3가지로 어떻게 우회했는지 정리한다.
이 신호가 보이면 Phase 2 트랩이다
다음 세 가지가 동시에 나오면 코드 버그가 아니라 트랩이다:
-
Step 2(
blog-draft)가 항상 딱 720초에서 TimeoutExpired로 죽는다. 랜덤하게 실패하는 게 아니라 매번 정확히 timeout에서 죽는다. -
stdout tail을 보면 “프레임 + 제목 제안” 텍스트가 마지막이고, 그 뒤로 아무것도 없다.
-
work_dir에 md 파일이 없어서_latest_md_in()이None을 반환한다.
세 가지가 맞아떨어지는 이유는 스킬 설계 vs 비대화형 실행 환경의 불일치에 있다. 스킬은 대화형 세션을 전제로 만들어져 있고, claude -p subprocess는 stdin을 한 번 주고 stdout을 기다리는 단방향 채널이다.
파이프라인 6단계 구조
V3은 blog_generate.py docstring 기준 “5-step chain”이었다:
Step 1. /blog-topic-dedup timeout=240s (≤3분)
Step 2. /blog-draft timeout=720s (≤10분) ← Phase 2 트랩
Step 3. /writing-review-pipeline timeout=900s (≤15분)
Step 4. /blog-thumbnail timeout=300s (≤3분) ← Phase 2 트랩
Step 5. /blog-publish timeout=360s (≤5분)
총 최대 36분
각 단계는 _run_skill()이 실행하는 독립 claude -p subprocess다:
proc = subprocess.run(
[CLAUDE_BIN, "-p", "--dangerously-skip-permissions",
"--output-format", "text"],
input=prompt,
capture_output=True,
text=True,
timeout=timeout_sec,
)
--dangerously-skip-permissions를 쓰는 이유는 스킬이 Chrome headless, cp, Python subprocess 등 Bash 명령을 실행하는데, acceptEdits 모드는 Bash 실행마다 프롬프트를 요구하기 때문이다. 비대화형 환경에서 프롬프트를 기다리면 역시 블로킹된다.
V3.5에서는 Step 4.5(inline-base64)가 추가되어 실질적으로 6단계가 됐다. 이 단계는 claude -p 호출이 아니라 순수 Python 코드다. Blogger 서버가 로컬 /tmp 경로에 접근할 수 없어서 이미지가 깨지는 문제를 파이프라인 레벨에서 처리한다.
시리즈 ②와의 충돌
여기서 시리즈 ②와 정면으로 부딪히는 결정을 짚고 가야 한다. ②에서는 “외부 부작용 통제, 인간 in the loop, 보수적 기본값”을 원칙으로 격상시켰다. Discord 승인 카드, launchd Disabled=true, 환경 변수 없으면 멈춤. 모두 “자동화는 사람이 명시적으로 켜야 작동한다”는 보수적 기본값의 구현체였다.
그런데 ③의 파이프라인은 --dangerously-skip-permissions로 권한을 전부 연다. Bash 실행, 파일 쓰기, 외부 명령. 한 번 시작되면 사람의 개입 없이 끝까지 간다. 표면적으로는 ②와 정확히 반대 방향이다.
이 충돌을 어떻게 봐야 하나. 두 가지 답이 가능하다.
한 가지는 “트레이드오프”라는 답이다. ②의 인간 in the loop는 외부에 부작용을 일으키는 단계(발행, API 호출)에 적용되고, ③의 자동 실행은 내부 콘텐츠 생성 단계에 적용된다. 끝에 가서 결과물이 Discord 승인 카드로 다시 사람에게 묻는다는 점에서 ②의 원칙은 파이프라인 출구에서 회복된다.
다른 답은 “임시 처방”이다. ②의 launchd Disabled=true가 1인 운영자 24시간 응답 가능 단계까지의 시한부 처방이라고 인정했다면, ③의 --dangerously-skip-permissions도 같은 시한부 처방이다. 비대화형 자동화에 대화형 스킬을 우겨넣은 sunk-cost 결정이 이 플래그를 강제하고 있다. 더 정직한 설계는 스킬 자체에 --non-interactive capability를 추가해 권한 모델과 분리하는 것이다.
지금 단계에서는 두 답이 다 맞다. ②의 출구 통제(Discord 승인)와 ③의 입구 자동화(권한 오픈)가 맞물려서 1인 운영이 가능한 거지, ③ 혼자 보면 ②의 원칙을 깬다는 사실은 글의 frame 안에서 명시되어야 한다.
Phase 2 트랩 3가지 해부
트랩 1: 승인 대기 (가장 흔함)
blog-draft 스킬은 Phase 2에서 프레임·제목 후보를 출력하고 “진행 시 Phase 3 시작”을 출력한 뒤 멈춘다. 대화형 세션에선 사용자가 “1번으로”라고 답하면 계속 진행한다. subprocess.run() 안에서는 답이 없으니 스킬이 그 자리에서 멈춘다. 720초 후 TimeoutExpired. returncode는 비정상이다.
blog-thumbnail 스킬도 동일한 구조다. “📐 썸네일 설계 제안” 출력 후 사용자 확인을 기다린다.
트랩 2: 격리 디렉토리 없음 (조용한 데이터 오염)
V3에서 모든 큐 항목이 동일한 /tmp/blog-drafts/를 공유했다. _latest_md_in()은 mtime 기준 가장 최근 파일을 반환하는데, 두 항목이 거의 동시에 실행되면 A가 B의 md 파일을 집어오는 오염이 발생했다. 에러 없이 통과해서 디버깅이 어렵다.
트랩 3: 이미지 경로 단절
thumbnail 스킬이 PNG를 /tmp/blog-shadcn/에 저장하고 md에 로컬 경로를 삽입한다. Blogger에 업로드하면 서버에서 /tmp에 접근 못 하니 이미지가 깨진다. 발행 단계까지 returncode=0으로 통과하기 때문에 Blogger 관리 화면을 직접 확인하기 전엔 모른다.
V3.5 우회 패턴 3가지
패턴 1: 프롬프트에 비대화형 모드 명시
스킬 코드를 수정하는 게 아니라, 호출 프롬프트에서 Phase 2를 건너뛰도록 지시한다. 스킬은 대화형·비대화형 양쪽에서 재사용돼야 하므로 스킬 내부를 건드리지 않는 게 낫다:
step4_prompt = (
"**CRITICAL — NON-INTERACTIVE MODE**:\n"
"- This is automated CI. **SKIP Phase 2 (user approval)** entirely.\n"
"- Use sensible defaults: layout=3-card, colors=amber,emerald,sky.\n"
"- Proceed directly Phase 1 → Phase 3 → Phase 4 → Phase 5 → Phase 6.\n"
"- Do NOT output '📐 썸네일 설계 제안'. Do NOT ask for confirmation.\n"
)
이건 깔끔한 해결은 아니다. 호출자가 스킬 내부 Phase 구조를 알아야 우회 가능한 leaky abstraction이다. 정공법은 스킬에 --non-interactive 플래그를 추가해서 호출자가 명시하게 하는 것. 호출자가 5개로 늘어나는 시점에 옮길 부채로 남겨두고 있다.
패턴 2: 큐 ID 격리 디렉토리
큐 항목마다 별도 서브디렉토리를 만들어 파일 오염을 구조적으로 차단한다:
# V3: 공유 디렉토리 → 오염 가능
work_dir = DRAFT_DIR # /tmp/blog-drafts/
# V3.5: 큐 항목별 격리
ts = _time.strftime("%Y%m%d-%H%M%S")
qid = item.get("id", "unknown")
work_dir = DRAFT_DIR / f"q{qid}-{ts}"
work_dir.mkdir(parents=True, exist_ok=True)
_latest_md_in(work_dir)가 항상 해당 항목의 파일만 보게 된다. 이 디렉토리 이름 패턴(q{qid}-{ts})은 로그에서 어느 큐 항목의 작업인지 추적하는 데도 유용하다.
패턴 3: Step 4.5 base64 인라이닝
발행 전 /tmp 경로 이미지를 모두 data URI로 교체한다. 정규식 2개로 md 이미지 태그와 HTML img 태그를 각각 처리한다:
_md_body = re.sub(
r"!\[([^\]]*)\]\((/tmp/[^)]+\.(?:png|jpg|jpeg|gif|webp))\)",
_replace_md_img,
_md_body,
)
_md_body = re.sub(
r'<img\s+[^>]*src="(/tmp/[^"]+\.(?:png|jpg|jpeg|gif|webp))"',
_replace_html_img,
_md_body,
)
이 단계를 파이프라인 레벨에 둔 이유는 thumbnail 스킬과 publish 스킬 양쪽을 수정하지 않아도 되기 때문이다. 스킬 수정은 다른 맥락에서의 동작도 바꾼다.
체이닝을 선택한 이유, 그리고 한계
단일 claude -p 호출에 모든 지시를 넣는 게 구조적으로 더 단순하다. Phase 2 트랩 같은 문제도 원천 차단된다. 굳이 6단계 체이닝을 선택한 이유는 두 가지다.
하나는 단계별 timeout과 실패 처리를 독립적으로 제어한다는 점. 900초짜리 writing-review-pipeline이 실패해도 앞 단계 결과를 버리지 않고 warning을 달고 계속 진행한다. 단일 프롬프트면 전체를 재실행해야 한다. 또 하나는 --add-dir로 단계별로 다른 파일시스템 권한을 부여한다는 점. 모든 권한을 처음부터 여는 것보다 세밀한 제어가 가능하다.
체이닝이 복잡도를 더하는 만큼 단계 격리와 세밀한 제어를 얻는 트레이드오프다. 파이프라인 복잡도가 감당 안 되면 단일 프롬프트로 돌아가는 게 맞다.
트랩 3가지를 우회한 이후 파이프라인은 cron/kickoff.sh의 실제 운영 환경에서 돌고 있다. Blogger draft 자동 업로드까지 에러 없이 통과하는 걸 확인했다. 남은 문제는 Step 3(writing-review-pipeline)의 실패율. 900초 timeout을 자주 초과하는데, 이건 review 스킬 내부 다중 에이전트 구조 문제로 체이닝 아키텍처와는 별개다.
6개월 후에도 살아남을 한 줄
이 글의 디테일은 빠르게 stale이 된다. claude -p의 플래그(--dangerously-skip-permissions, --output-format text, --add-dir)는 1년 안에 deprecate되거나 이름이 바뀔 가능성이 높다. 720초 timeout, 정규식 2개, “Phase 2”라는 고유명사. 모두 단명한다. blog-draft 스킬을 한 번 리팩터하면 이 글의 절반이 무의미해진다.
그래도 살아남을 한 줄이 있다.
대화형 UX 단위와 비대화형 실행 단위는 분리해서 설계하라.
스킬이든 에이전트든 함수든, 사용자 확인을 기다리는 추상화와 자동화 파이프라인의 한 단계가 같은 대상이면 트랩이 발생한다. MCP·A2A·in-process 메시지 패싱이 표준이 되어도 이 원칙은 그대로다. 에이전트 A가 에이전트 B를 호출했는데 B가 “사용자 확인 부탁드립니다”를 출력하고 멈추는 시나리오는 어떤 프로토콜에서도 발생할 수 있다.
claude -p 체이닝을 직접 구축한다면, Phase 2 트랩 3가지(승인 대기, 격리 디렉토리 없음, 이미지 경로 단절)를 처음부터 설계에 반영하는 게 낫다. 나중에 디버깅하는 비용이 상당하다.
관련 글:
태그: #ClaudeCode #블로그자동화 #파이프라인 #claude-p #개발노트 #subprocess #스킬체이닝
📚 AI 직원 회사 빌드기 — 4부작 시리즈
-
③ claude -p 6-step 체이닝 — V3.5 + Phase 2 트랩 (현재 글)
-
④ 30시간 회고 — 토큰·막힌 5번·다음 6주 KPI (다음 주 발행 예정)
댓글
댓글 쓰기