
무인 Claude Code가 아무도 거부하지 않았는데 조용히 멈추는 현상 (2.1.210) —— 무서워서 내 hook을 58개 전부 다
요약
Claude Code 무인 세션이 사용자 거부 없이 조용히 멈추는 버그와 그 원인을 분석합니다. 특정 버전(2.1.210 미만)에서 hook 콜백 타임아웃이 모델에게 사용자 거부로 잘못 전달되는 문제를 다룹니다.
핵심 포인트
- Claude Code 2.1.210 미만 버전에서 hook 타임아웃 시 세션이 멈추는 버그 존재
- 버그의 핵심 조건은 '버그'와 '느린 hook'의 결합
- 네트워크 호출이나 무거운 스캔을 포함한 hook은 타임아웃 위험이 높음
- 해결책으로 최신 버전 업데이트 및 hook 성능 점검 권장
Claude Code를 무인으로 돌리고 있다. 밤사이에 긴 작업을 맡겨두고, 아침에 결과를 확인한다. 그러한 운용 방식에서 가장 질이 나쁜 고장 방식이 있다.
다운되는 것이 아니다. 에러를 뱉는 것도 아니다. 아무도 거부하지 않았는데, 조용히 멈춰서 기다린다. 다운된다면 알아챌 수 있다. 에러가 나오면 추적할 수 있다. 하지만 「멈춰서 기다리는 것」은 정상적인 일시 정지와 구별할 수 없다. 밤을 새울 생각이었는데, 처음 몇 분 만에 멈춰서 아침까지 아무것도 하지 않았다――이런 일이 일어날 수 있다.
2.1.210의 업데이트 이력에 그 정체가 적혀 있었다.
Fixed a hook callback timeout being misreported to the model as a user rejection, which made unattended sessions stop and wait
「hook의 콜백 타임아웃 (callback timeout)이 모델에게 '사용자에 의한 거부'로 잘못 전달되어, 무인 세션이 멈춰서 기다리게 만들던 문제를 수정했다.」 이 부분은 공식 업데이트 이력을 출처로 한 전언이며, 직접 의도해서 재현한 것은 아니다. 내 손에 있는 버전은 2.1.202로, 이 수정 이전 버전이다. 그래서 아직 이 동작이 남아 있는 쪽에 있다.
이 글을 읽고 소름이 돋았다. 우리 쪽은 hook을 대량으로 쌓아두고 있다. 그중 하나라도 응답이 늦어지면, 거부로 오인되어 밤중에 조용히 멈춰버린다. 그렇게 생각되어 내 환경을 세어보았다.
내 환경에서 세어보니, ~/.claude/settings.json에 등록된 hook은 58개였다. 그중 타임아웃 (timeout)을 명시한 것은 0개. 전부 기본 타임아웃에 의존하고 있었다. 여기서부터는 실측――내 환경에서 하나씩 열어서 확인한 이야기다.
숫자만 보면 최악이다. 58개의 응답이 모두 기본 상한선에 도박을 걸고 있다. 하지만 숫자만으로는 아무것도 알 수 없다. 중요한 것은 숫자가 아니라 느린 정도다. 타임아웃의 입구가 되는 것은 응답이 늦어지는 hook――네트워크를 호출하는 것, 무거운 집계를 하는 것, 외부 커맨드 (command)의 완료를 기다리는 것이다. 그래서 하나씩 내부를 열어보았다.
처음에 걸린 것은 curl이나 wget, gh를 포함하는 hook이 10개 정도 있었다는 점이다. 네트워크를 호출한다면 이것이 가장 유력한 용의자가 된다.
하지만 열어보니 상황이 달랐다. 모두 내가 네트워크를 호출하는 것이 아니었다. 「위험한 네트워크 커맨드를 막기」 위해, 실행하려는 커맨드 문자열을 대조하고 있을 뿐이었다. 비밀 정보 유출을 막는 hook, 외부로의 전송을 경고하는 hook. 이름에 curl이 등장하지만, curl을 실행하는 것은 아니다. 문자열을 보고 즉시 통과시킬지 막을지를 결정한다. 빠르다.
무거운 스캔을 하는 hook도 찾았다. 하나, git의 깨진 잔해를 세는 hook이 find를 사용하고 있었다. 하지만 스캔 범위는 .git/objects 아래로 한정되어 있어서, 홈 디렉터리 전체를 훑는 식의 동작은 하지 않았다. 이것도 빠르다.
결론. 우리 쪽의 58개는 전부 빠른 로컬 대조였다. 네트워크도, 무거운 스캔도, 외부 대기도 없다. 따라서 이 버그에 대한 우리 쪽의 노출도는 낮다.
솔직히 허탈했다. 하지만 점검을 해보고 나서야 비로소 그렇게 말할 수 있었다. 세어보기 전에는 「58개·타임아웃 0」이라는 글자만 보고 겁을 먹고 있었을 뿐이었다.
여기가 요점이다. 이 고장 방식은 버그 단독으로는 이빨을 드러내지 않는다. 버그 × 느린 hook이 되어야 비로소 무인 세션을 멈춘다. 빠른 hook만 쌓아두고 있다면, 2.1.210 이전이라도 이 오보의 입구를 밟기 어렵다. 반대로 외부를 호출하는 hook을 타임아웃 없이 쌓아두었다면 그곳이 급소가 된다.
그러므로 대비책은 자신의 hook 내용에 따라 결정된다. 사람마다 다르다.
첫째, 버전을 올린다. 2.1.210 이후라면 업데이트 이력에 따라 이 오보는 수정되었다. 우선 이것이 근본이다.
둘째, 그것과는 별개로 자신의 hook을 세어보고, 느린 것을 지목한다. 네트워크를 호출하거나, 무거운 집계를 하거나, 외부의 완료를 기다리는――짐작 가는 것이 있다면 그 hook에 timeout으로 덮개를 씌운다. 응답이 오지 않으면 기본값으로 빠르게 반환한다. 응답이 내부 상한을 넘지 않는다면 애초에 오보의 입구를 밟지 않는다.
무료인 cc-safe-setup에는 이 주변에 유효한 예가 있다. 오래 걸릴 수 있는 커맨드를 시간 상한 없이 실행하려 할 때 경고하는 bash-timeout-guard.sh
、무인으로 실행 중인 프로세스의 생존 여부를 감시하는 dispatch-liveness-watchdog.sh
솔직히 말하자면, 이 오보(false positive) 자체를 막는 hook은 작성할 수 없다. 실행 시점의 내부적인 오보이며, 사용자의 hook 측에서는 개입할 수 없는 구조이기 때문이다. 하지만 응답 지연의 원인을 줄이는 것과, 멈춰버린 무인 세션을 알아차리는 것은 수동으로 할 수 있다.
프로세스가 종료(crash)되는 것보다, 말없이 멈춰버리는 것이 무인 운영에서는 더 까다롭다. 정상적인 일시 정지와 구분이 되지 않기 때문이다. 2.1.210 이전의 Claude Code는 hook의 응답 지연을 사람에 의한 거절(rejection)로 오인하여, 무인 세션을 조용히 중단시켰다 (업데이트 이력에 근거함).
대비책은 버전을 올리는 것. 자신의 hook을 세어보고, 느린 것들에 timeout을 설정하여 제한을 두는 것. 그리고 멈춘 것을 알아챌 수 있는 감시 장치를 하나 두는 것이다. 다행히 나의 58개 hook은 전부 빨랐다. 하지만 세어보기 전까지는 알 수 없었다. 무인으로 돌릴 생각이라면, 먼저 자신의 발밑을 점검하는 것이 좋다.
"사라졌다", "멈췄다", "고장 났다"라고 보이는 순간, 실제로 무엇이 일어나고 있는가. 보고와 실태가 어긋나는 유형—성공했다고 거짓말하기, 거절과 오보하기, 사라진 것처럼 보이지만 남아있기—은 공개된 이슈(issue)를 통해 차례차례 보고되고 있다. 이를 하나씩 1차 검증하여 증상별로 정리한 책을 쓰고 있다. 『Claude Code 사고 방지 핸드북』 (¥800 · 제3장까지 무료). 무인으로 길게 실행하는 사람들을 위한 한 권이다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기