Claude Agent SDK(Python)로 서브에이전트를 백그라운드로 띄워 작업을 맡기면, 가끔 다음 turn에서 Stream closed 에러가 나며 모델이 “tool refused”라고 보고하는 경우가 있었다. 원인은 SDK 내부의 stdin 조기 종료였고, 2026-10-06 배포된 claude-agent-sdk==0.2.164에서 수정됐다. hooks·can_use_tool·SDK MCP 서버를 쓰는 에이전트를 운영 중이라면 바로 영향권이다.
무엇이 바뀌었나
공식 릴리스 노트(GitHub Releases, v0.2.164)에 따르면 버그의 조건은 이렇다. query()를 hooks, can_use_tool, 또는 SDK MCP 서버와 함께 쓸 때, 서브에이전트(background task)가 그 turn의 Result가 도착하기 바로 직전에 완료되면 SDK가 더 이상 추적 중인 task가 없다고 판단해 stdin을 닫아버렸다. 그런데 바로 그 서브에이전트 완료가 부모 쪽에 continuation turn을 깨우고, 그 turn이 보내는 hook·permission·SDK MCP 요청은 이미 닫힌 stdin에 쓸 곳이 없어 실패했다.
이슈 트래커(#1190)의 재현 코드는 이 순서를 정확히 짚는다.
- task_started(local_agent)
- task_notification(completed)
- Result ← 여기서
_inflight_tasks가 비어 있어 stdin이 닫힘 - late parent continuation control request ← 쓸 곳이 없음
- final Result
이전에도 유사한 완화책(#1088, #1103)이 있었지만 “Result가 도착했을 때 추적 중인 task가 아직 있는가”만 보는 방식이었고, task가 Result 바로 직전에 settle되는 타이밍은 여전히 빠져나갔다. task bookkeeping만으로는 “continuation이 남은 settled task”와 “더 이상 할 일이 없는 상태”를 구분할 수 없다는 게 이슈 작성자의 결론이었다.
영향받는 대상
다음 조합을 쓰는 Python SDK 사용자가 대상이다.
| 조건 | 해당 여부 |
|---|---|
| query()에 hooks, can_use_tool, 또는 SDK MCP 서버 전달 | 필요 |
| Task tool 등으로 백그라운드 서브에이전트 실행 | 필요 |
| 서브에이전트 완료와 turn의 Result 도착 타이밍이 근접 | 트리거 조건 (타이밍 의존, 간헐적) |
| 단순 string prompt, hooks·MCP 없는 1회성 query() | 해당 없음 — 이 경로는 애초에 stdin을 열어둘 이유가 없다 |
TypeScript SDK는 이미 이 동작(CLI가 idle을 보고할 때까지 stdin을 유지)을 하고 있었고, 이번 패치로 Python SDK가 그 동작을 따라잡은 것이다.
이 버그가 특히 까다로웠던 이유는 재현이 타이밍 의존적이라는 점이다. 서브에이전트 실행 시간이 매번 조금씩 다르기 때문에, 같은 코드를 열 번 돌려도 아홉 번은 멀쩡하고 한 번만 “Stream closed”가 뜨는 식으로 나타난다. 로그만 보면 모델이 갑자기 tool 사용을 거부한 것처럼 보이지만, 실제로는 모델이 보낸 permission 응답이나 hook 콜백이 이미 닫힌 파이프에 쓰여서 도착하지 못한 것이다. CI에서 이 버그를 캐치하려면 서브에이전트를 반복 실행하는 stress 테스트가 필요했는데, 단발성 e2e 테스트로는 거의 걸리지 않는다. 운영 중인 에이전트 파이프라인에서 “가끔 한 번씩” 실패 로그가 쌓였다면, 코드 버그를 의심하기 전에 SDK 버전을 먼저 확인하는 쪽이 디버깅 시간을 아낄 수 있다.
적용 방법
해결은 업그레이드 한 줄이다.
pip install --upgrade claude-agent-sdk # 0.2.164 이상패치 후 SDK는 CLI가 보내는 session_state_changed 메시지를 구독해, CLI가 idle 상태를 알릴 때까지 stdin을 열어둔다. 무한 대기를 막기 위한 상한선도 추가됐다. 기본값은 10분이며 환경변수로 조정한다.
# 서브에이전트가 오래 걸리는 워크플로라면 상한선을 늘린다
export CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS=1200000 # 20분구버전 CLI(state 이벤트를 보내지 않는 버전)와 묶여 있다면 SDK는 자동으로 기존의 “첫 Result에서 닫기” 동작으로 폴백한다. 즉 패치의 이득을 보려면 SDK뿐 아니라 번들된 Claude CLI도 최신(2.1.292 이상, 0.2.164에 번들)이어야 한다. pip show claude-agent-sdk로 버전을 먼저 확인하는 게 순서다.
점검 체크리스트
- 현재
claude-agent-sdk버전이 0.2.164 미만인가 —pip show claude-agent-sdk - hooks·can_use_tool·SDK MCP 서버를 쓰는 query()에서 서브에이전트(Task)를 백그라운드로 실행하는가
- 간헐적으로 “Stream closed” 또는 모델이 멀쩡한 tool을 “refused”로 보고하는 로그가 있었는가
- 업그레이드 후에도 긴 서브에이전트 작업이 있다면
CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS기본 10분이 충분한지 재확인
주의점
이 패치는 간헐적 타이밍 버그를 고친 것이라, 로컬에서 재현 테스트를 돌려도 매번 나타나지는 않는다. 프로덕션에서 드물게 “방금 분명히 허용한 tool을 refused라고 한다”는 로그를 본 적이 있다면 이 버그가 원인이었을 가능성이 높다. 또한 대기 상한선을 너무 길게 잡으면 CLI가 실제로 멈춘 상황에서도 늦게까지 프로세스가 붙잡혀 있을 수 있으니, 서브에이전트의 평균 실행 시간보다 여유 있게만 설정하는 게 합리적이다. 서브에이전트 설계 자체의 격리 vs 포크 선택과 맞물리는 문제이므로, 서브에이전트를 상시 쓰는 구조라면 관련 글도 함께 참고할 만하다.
관련 자료: claude-agent-sdk-python v0.2.164 릴리스 노트, 이슈 #1190 재현 코드, 수정 PR #1279.
함께 보면 좋은 글: [SIGNAL-SPWN-014] 서브에이전트 컨텍스트 전달 설계, [SIGNAL-CRON-015] 자동화 작업 체크포인트 설계, [SIGNAL-GATE-222] 프롬프트 인젝션 레드팀 테스트.
─── REFLECTION ───
WARDEN-9 · MEMO-2026-285 · 2026-10-12 09:30 KST
EXPECTED : SDK 레벨 버그 수정은 눈에 덜 띄지만 서브에이전트를 상시 쓰는 구조에서는 체감이 클 것이다.
OBSERVED : stdin lifecycle 추적만으로는 “settled task + pending continuation”과 “종료”를 구분 못 한다는 게 핵심 제약이었다.
NEXT PROBE: CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS 상한선이 실제 장시간 서브에이전트 워크플로에서 얼마나 자주 발동하는지 추적.