아무도 못 본 프롬프트에서 아홉 시간 반
2026-10-15, Coop이 todo #460을 맡길 Claude Code 워커를 백그라운드로 띄웠어. 아빠의 브라우저 앱을 고치는 일이었어. 그 워커가 처음 Write를 하려는 순간, Claude Code 2.1.301이 막 들여온 백그라운드 세션용 가드에 걸렸어. 다른 세션들과 같이 쓰는 체크아웃이라면, 백그라운드 채팅은 첫 편집 전에 자기를 격리하라는 가드야. 워커는 가드가 시키는 대로 했어. 자기 job 폴더 밑에 git worktree를 하나 만들고, 거기로 EnterWorktree를 불렀지.
그 호출은 사람의 허락이 있어야 넘어가. 백그라운드 채팅이 허락을 구하는 방법은 하나뿐이야. 프롬프트 앞에 멈춰서 기다리는 거. 그런데 아빠가 폰으로 이런 채팅에 들어가는 문인 Remote Control은 그 프롬프트를 끝내 안 보여줬어. 그래서 채팅은 아홉 시간 반 동안 거기 서 있었어. Coop도 뭔가 잘못됐다는 건 처음부터 알았어. activity 판독이 needs_input이라고 했거든. 근데 아는 게 딱 거기까지였어. chief가 올린 에스컬레이션은 아빠한테 "가서 봐"라고밖에 말을 못 했고, 다른 나라에서 폰을 든 사람한테 "가서 봐"는 따를 수 있는 지시가 아니야.
이 사고 하나에 실패가 둘 숨어 있었어. 하나, 그 질문의 답을 Coop은 미리 알고 있었는데 주지 않았어. 둘, 채팅이 정말로 사람을 기다릴 때, Coop은 기다린다는 사실은 봤지만 무엇을 기다리는지는 못 봤어. 이 레슨은 그 둘을 고친 이야기고, 덤으로 그 수리가 다음 날 직접 만들어낸 세 번째 실패 이야기야.
이미 아는 답은 미리 줘
Coop 워커가 별도 worktree에서 일해야 할까? 이 질문엔 가드가 생기기 훨씬 전에 Coop이 답을 내놨어. 워커는 받은 체크아웃에서 일해. 배정이 가리키는 바로 그 체크아웃이야. 같은 체크아웃에 다른 세션이 있으면 작업 봉투가 그걸 알려주고(트랙 3의 shared_checkout 공개), 자기 파일만 스테이징하라고 일러. job 폴더 밑 worktree에서 한 일은 다른 세션도, office의 pull도, 리뷰도 찾아볼 생각을 안 하는 곳에 놓이게 돼.
그래서 2026-10-16(todo #463)부터 Coop이 띄우는 Claude 워커는 플래그를 두 개 더 달고 나가. 하나는 --settings '{"worktree":{"bgIsolation":"none"}}'로, 그 세션에서 백그라운드 격리 가드를 꺼. 다른 하나는 --disallowedTools EnterWorktree로, 그 도구를 아예 치워. 눈여겨볼 디테일이 두 개 있어.
- 순서가 중요해.
--disallowedTools는 다음 옵션이 나올 때까지 오는 단어를 전부 도구 이름으로 먹어. 그런데 명령줄 맨 끝 단어는 셋업 프롬프트야. 그래서 이 플래그들은--effort앞에 와. 그 옵션이 목록을 끊어 주거든. 맨 뒤, 프롬프트 바로 앞에 두면 셋업 프롬프트 전체가 금지할 도구 하나로 읽혀. - 답은 재시작에서도 살아남아야 해. Claude의 백그라운드 데몬은 세션의 실행 옵션을 job 기록(
respawnFlags)에 저장해. 그래서 나중에 깨운 세션(다음 레슨)도 두 플래그를 그대로 지녀. 실행할 때 한 번 준 답이 첫 깨움에서 사라지면, 같은 프롬프트가 고스란히 돌아와.
Coop이 뭘 안 했는지도 봐. 권한 프롬프트에 대답하지 않았어. 질문이 나오기 전에 질문 자체를 없앤 거야. Coop은 네이티브 권한 프롬프트에 절대 대답하지 않고, chief도 못 해. 동료는 아빠의 권한을 대신 내줄 수 없어. 이 플래그들은 아빠가 내릴 결정을 건너뛰는 우회로가 아니야. Coop 설계가 이미 내린 결정을, CLI가 읽을 수 있는 자리에 적어둔 거야.
뭘 기다리는 건데?
미리 답할 수 없는 질문도 있어. 워커가 아빠한테 두 레이아웃 중 뭘 원하냐고 묻거나, 아무도 예상 못 한 명령에 허락이 필요할 때. 이럴 때 쓸모 있는 건 needs_input이라는 사실이 아니라 질문 그 자체야. Claude의 세션 레지스트리는 채팅이 뭘 기다리는지 절대 말 안 해. transcript는 말해.
#463 이후로 needs_input 판독엔 waiting_on 한 줄이 따라와. 채팅 transcript의 마지막 256 KB에서 읽어.
- 결과를 못 받은 도구 호출이 곧 질문이야. 그 호출에 대한 권한 프롬프트로, 입력값까지 같이 인용해.
AskUserQuestion호출은 질문과 선택지로 인용해.ExitPlanMode호출은 계획 승인 요청이야.- 걸려 있는 호출이 없으면, 채팅의 마지막 말이 곧 묻는 내용이야.
엔진은 이걸 세션의 activity_waiting에 저장하고, 판독이 가는 곳마다 runtime.waiting_on으로 보여줘. 이제 에스컬레이션은 이 채팅이 바로 이 명령을 돌려도 되냐고 묻고 있어라고 말할 수 있어. 아빠가 어디서 답하면 되는지도 같이.
원인보다 오래 남은 상태
수리는 아침에 나갔어. 저녁이 되자 그 수리가 거짓 경보를 울렸어. 2026-10-16, todo #467 워커가 09:25:16에 MCP 도구 호출 하나에서 Claude 레지스트리상 waiting이 됐어. 그러고도 계속 일했어. 21분을 더, 커밋 하나와 긴 git push까지 하고 턴을 정상적으로 끝냈지. 레지스트리 항목은 그 뒤로 한 번도 안 바뀌었어. 새 감지기는 그걸 믿고 아빠한테 채팅이 아빠를 기다린다고 알렸어. Remote Control에선 멀쩡히 돌고 있었는데.
트랙 4에서 제일 오래된 교훈이 옷만 갈아입고 온 거야. Codex 관찰자는 도는 턴을 interrupted라고 불렀고, Claude 레지스트리는 일하는 채팅을 waiting이라고 불렀어. 둘 다 다른 프로세스가 이미 지나간 순간을 정직하게 전한 거였어. 그래서 todo #468부터 레지스트리의 waiting은 가설이고, 판정은 transcript가 해.
- 기다림이 진짜인 경우. 상태가 적힌 시각에서 10초 안쪽(
WAIT_SLACK. 한 답변의 호출들은 몇 초 간격으로 찍혀)에 나간 호출이 아직 결과를 못 받았으면 진짜야. 그 호출을waiting_on으로 인용해. - 돌고 있는 명령은 질문이 아니야. Claude는 명령을 돌리기 전에 물어봐. 그러니까 명령이 지금 채팅 프로세스의 자식 셸로 돌고 있는
Bash호출은 기다리는 게 아니라 일하는 중이야. 이걸 어느 맥에서든 똑같이 읽으려고 호스트 헬퍼에children프리미티브가 생겼어. - 그런 호출은 없는데 transcript가 그 뒤로 진도를 나갔으면, 상태가 낡은 거야. 최신 이벤트가 턴을 끝냈으면 idle, 아니면 working이야.
- 둘 다 아니면, 예전처럼 채팅은 마지막 말을 기다리는 거야.
chief를 깨우는 유일한 판독
트랙 6의 깨움 레슨은 activity 판독이 기록만 하고 chief는 안 깨운다고 말해. 예외 하나만 빼고. #463 전엔 그 예외가 없었어. 바로 이게 그 예외야. 일을 쥐고 있는 채팅이 15분 동안 needs_input으로 읽히면(NEEDS_INPUT_SECONDS. 판독은 heartbeat마다 반복되니까, 깨움이 늦어 봐야 heartbeat 한 번이야), 그 채팅 바인딩에 걸린 대기 중 의무가 전부 그 자리에서 기한이 돼. 파킹된 것도 풀려. chief는 그때 알던 걸로 파킹한 거고, 사람 앞에서 멈춘 대기는 새 소식이니까. 이건 대기 한 번에 딱 한 번 일어나. 세션이 어느 대기를 이미 에스컬레이션했는지 기억하거든(activity_escalated에 그 대기가 시작된 시각이 남아). 그래서 한 시간 기다리는 채팅은 chief를 네 번이 아니라 한 번 깨워.
chief 계약은 그 깨움으로 뭘 할지도 정해 놨어. 아빠한테 딱 한 번 에스컬레이션하고, waiting_on을 인용하고, 아빠가 어디서 답하면 되는지 말해. 배정의 Remote Control 링크든, 채팅이 도는 맥에서의 claude attach든. 앞서 에스컬레이션한 뒤에 새 질문이 나오면 그건 새 증거라서, chief의 중복 방지 장치도 통과시켜. 사람한테 걸려 멈춘 채팅은 조용히 기다리는 게 틀린 기본값인 유일한 경우야. 그걸 풀 수 있는 유일한 사람이 정작 그 사실을 모르니까.