요즘IT
위시켓
AIDP - AX
Rise ERP
콘텐츠프로덕트 밸리
요즘 작가들컬렉션물어봐
놀이터
콘텐츠
프로덕트 밸리
요즘 작가들
컬렉션
물어봐
놀이터
새로 나온
인기
개발
AI
IT서비스
기획
디자인
비즈니스
프로덕트
커리어
트렌드
스타트업
서비스 전체보기
위시켓요즘ITAIDP - AXRise ERP
고객 문의
02-6925-4867
10:00-18:00주말·공휴일 제외
yozm_help@wishket.com
요즘IT
요즘IT 소개작가 지원
기타 문의
콘텐츠 제안하기광고 상품 보기
요즘IT 슬랙봇크롬 확장 프로그램
이용약관
개인정보 처리방침
청소년보호정책
㈜위시켓
대표이사 : 박우범
서울특별시 강남구 테헤란로 211 3층 ㈜위시켓
사업자등록번호 : 209-81-57303
통신판매업신고 : 제2018-서울강남-02337 호
직업정보제공사업 신고번호 : J1200020180019
제호 : 요즘IT
발행인 : 박우범
편집인 : 노희선
청소년보호책임자 : 박우범
인터넷신문등록번호 : 서울,아54129
등록일 : 2022년 01월 23일
발행일 : 2021년 01월 10일
© 2013 Wishket Corp.
로그인
요즘IT 소개
콘텐츠 제안하기
광고 상품 보기
개발

클로드 코드 hook 재귀 루프 사고로 토큰 60M 날리고 배운 것

개발자H
7분
0시간 전
55
에디터가 직접 고른 실무 인사이트 매주 목요일에 만나요.
newsletter_profile0명 뉴스레터 구독 중

클로드 코드에서 세션을 닫을 때, AI가 할 일 목록을 알아서 정리해주면 편하겠다 싶었어요. 그 아이디어 하나 때문에 토큰을 60M 넘게 태우고 세션 한도(session limit)를 통째로 날려먹었습니다.

 

무슨 상황이었냐면요. 전 클로드 코드에서 어떤 작업이 끝나면 그 결과에 맞춰 다른 폴더의 작업 현황을 동기화하는 자동화를 걸어뒀습니다. 그런데 그 자동화로 종료 시점에 새 클로드 코드 세션을 하나 띄우게 돼 있었어요. 이 새로운 세션이 끝나면서 같은 자동화를 다시 탔고, 그게 또 새로운 세션을 띄웠습니다. 종료가 종료를 낳는 구조였던 거죠. 그래서 미친듯이 토큰이 낭비된 겁니다.

 

이 글은 그 사고를 처음부터 복기한 기록이에요. 뭔가 대단한 버그를 잡은 이야기는 아니에요. 오히려 며칠 동안 로그를 한참 뒤져서야 겨우 진범을 찾은, 좀 창피한 축에 가까운 이야기입니다. 그래도 에이전틱 하네스(agentic harness)를 쓰다 보면 한 번쯤 마주칠 함정이라, 경험을 겪은 그대로 공유하는 게 도움이 되겠다 싶어 정리했습니다.

 

‘ON’ 녹색 토글 스위치와 연결된 게이지가 ‘IMPACT LEVEL 0~100’ 눈금에서 100을 가리키는 사진
<출처: 작가, GPT로 생성>
 

엉뚱한 데를 의심한 며칠

사고를 알아차린 건 2026년 7월 3일 오전이었어요. 그날 시킨 작업 자체는 별거 없었습니다. 개인 폴더에 루틴이랑 메모 몇 개 만들어달라는, 몇 분이면 끝날 일이었죠. 그런데 잠깐 뒤에 사용량을 보니까 토큰이 비정상적으로 올라가 있었고, 곧 세션 한도에 걸리더라고요.

 

이럴 때 보통 가장 최근에 바꾼 걸 먼저 의심하잖아요. 저도 그랬습니다. 짚이는 변경이 두 개 있었거든요.

 

하나는 설치 방식이었어요. 얼마 전에 홈브루(Homebrew)로 관리하던 클로드 코드를 지우고 npm install -g로 다시 깔았거든요. 재설치하다 뭔가 꼬였나 싶었죠. 다른 하나는 모델이었습니다. 그 무렵 Sonnet 5이 새로 나와 쓰고 있었는데, 새 모델이 예상보다 토큰을 많이 먹나 하는 의심이 들더라고요.

 

둘 다 그럴듯했는데, 둘 다 틀렸어요. 설치 방식이든 모델이든, 작은 작업 하나가 그 정도 토큰을 쓸 이유가 안 됩니다. 작업은 작은데 사용량은 크다는 이상함이 결국 저를 다른 쪽으로 돌려세웠어요. 제가 시킨 작업이 원인이 아니라, 그 작업이 끝난 다음에 벌어지는 뭔가에 있었던 거죠.

 

 

진범은 git history 두 단계 뒤에

방향을 바꾸고 제일 먼저 열어본 건 프로젝트의 .claude/settings.json이었어요. 거기 SessionEnd 훅(hook)이 걸려 있더라고요. 세션이 끝나면 .claude/hooks/session-end-sync.sh라는 스크립트를 실행하게 돼 있었고, 그 스크립트 안에 이런 코드가 있었습니다.

 

nohup claude -p "$(cat "$PROMPT_FILE")" >> "$LOG" 2>&1 &disown $!

 

클로드 코드 세션이 끝날 때마다 새 클로드 코드 세션을 백그라운드로 띄우는 코드였어요. 이걸 보는 순간 대략 감은 왔는데, 확신이 필요해서 깃 히스토리(git history)를 거슬러 올라갔습니다. 그런데 며칠 전에 만든 훅이 왜 하필 오늘 터졌는지가 설명이 안 되더라고요. 커밋 로그를 보고 나서야 이해했습니다. 이 훅이 한 번에 이 모습으로 나온 게 아니라, 두 단계를 거쳐 위험한 상태로 바뀌어 있었던 겁니다.

 

1단계는 6월 30일의 작업에서 이뤄졌어요. SessionEnd에 type: agent 훅이 처음 들어갔습니다. 세션 종료 때마다 LLM 에이전트(agent)가 도는 구조라 좀 위험하긴 했는데, 이건 그래도 하네스 안쪽에서 도는 훅 에이전트 실행에 가까웠어요.

 

2단계는 7월 1일 밤 11시 54분이었습니다. type: agent가 type: command로 바뀌면서, command가 앞서 본 session-end-sync.sh를 실행하게 됐어요. 이 한 줄이 위험도를 완전히 다른 차원으로 끌어올렸습니다. 훅 안에서 claude -p로 새 CLI 프로세스를 띄우는 순간, 훅이 더 이상 생명주기(lifecycle)에 붙은 단순 스크립트가 아니라 새 세션 생성기가 돼버린 거죠.

 

그러니까 진범은 Sonnet 5도, npm 재설치도 아니었어요. 며칠 전 밤에 만들어진 command 훅이었죠. 다른 날 개인 폴더 작업이 끝나면서 SessionEnd가 발화한 순간, 재귀가 본격적으로 드러난 거예요. 그 전날부터 이상 사용량이 이미 쌓이고 있다가 제가 확인한 아침에야 눈에 보일 만큼 커졌다고 봐야 하고요.

 

 

재귀가 돈 흔적, 로그에 남은 숫자들

돌이켜보면 흐름은 단순해요.

 

사용자 세션이 종료된다 → SessionEnd 훅이 실행된다 → 훅이 claude -p로 새 세션을 백그라운드로 띄운다 → 새 세션도 같은 프로젝트 폴더와 같은 settings.json을 로드한다 → 새 세션이 끝나면서 다시 SessionEnd를 탄다 → 다시 claude -p가 실행된다.

 

문제는 이 순환이 멈출 이유가 코드 어디에도 없었다는 거예요.

 

멈춤 이유가 어디에도 없던 순환: 사용자 세션 종료 → SessionEnd hook 실행 → claude -p로 새 세션 백그라운드 실행 → 새 세션이 같은 settings.json 로드 → 새 세션도 종료돼 다시 항목 2로 이어지는 5단계 무한 순환 다이어그램
<출처: 작가, GPT로 생성>

 

사용량 그래프가 이 상황을 그대로 보여주더라고요.

 

7월 1일 하루 총 사용량은 약 5.3M 토큰이었어요. 그런데 7월 2일은 약 60.4M 토큰으로 뛰었고, 3일은 오전부터 이미 15.4M 토큰을 넘겼습니다. 특히 눈에 띈 건 7월 2일의 캐시 읽기(cache read)였어요. 60.4M 중 약 58.5M이 캐시 읽기였는데, 자동으로 생겨난 세션들이 같은 프로젝트 컨텍스트(context)랑 에이전트 목록, 스킬(skill) 목록, 훅 프롬프트(prompt)를 계속 반복해서 읽었다는 뜻으로 보입니다. 세션 하나하나가 매번 같은 걸 읽으며 새로 부팅한 셈이죠.

 

‘하루 만에 5.3M에서 60.4M으로’ 막대그래프: 7월 1일 5.3M 토큰, 7월 2일 60.4M 토큰(cache read 58.5M 포함), 7월 3일 오전 15.4M 토큰
<출처: 작가(GPT 생성)>

 

세션 로그도 확인했어요. 클로드 코드가 세션마다 남기는 JSONL 로그 디렉터리 크기가 약 3.3GB였습니다. 그 안의 JSONL 세션 파일 수는 137,305개였고요. 이 중 7월 3일 오전 7시 이후에 생긴 것만 9,101개였는데, 그 9,101개 중에 8,466개가 세션 한도나 rate_limit 흔적을 담고 있었어요. 어떤 구간에서는 1분에 100개 넘는 세션 로그가 만들어졌습니다.

 

파일을 하나하나 열어보니까 사용자 프롬프트 자리에 제가 쓴 요청이 아니라 훅이 넣은 문장이 반복돼 있더라고요. “세션이 종료됩니다. task board를 이 세션의 작업 결과에 맞게 동기화하세요” 같은, 제가 훅에 심어둔 지시가 수천 번 되풀이하고 있었죠. 개인 폴더 작업이 토큰을 다 쓴 게 아니라, 그 작업이 끝난 뒤에 훅이 만들어낸 세션들이 태운 거였어요.

 

그날 맥북 팬이 유난히 시끄럽고 뜨거웠던 것, 파인더(Finder)가 CPU를 100% 넘게 잡아먹던 것도 이 사고랑 무관하지 않았어요. 직접 원인이야 백그라운드에서 계속 뜨는 claude -p 프로세스였겠죠. 다만 파인더가 CPU를 먹은 건 짧은 시간에 작은 JSONL 파일이 수천, 수만 개씩 쏟아지면서 macOS의 파일 시스템 UI 계층이 그 변화를 따라가느라 생긴 2차 효과 같습니다. 용량 자체보다 짧은 시간에 작은 실패 세션이 수천 개 생긴 패턴이 시스템 전반을 흔든 거죠.

 

 

hook과 loop의 결정적 차이

이 사고의 본질은 결국 훅을 루프(loop)처럼 써버린 데 있었다고 생각합니다.

 

에이전틱 하네스를 다룰 때 저는 이걸 프롬프트, 컨텍스트, 도구(tool), 훅, 루프 다섯으로 나눠서 봐요. 모델에 주는 지시가 프롬프트, 모델이 참고하는 환경이 컨텍스트, 모델이 쓰는 능력이 도구, 하네스의 생명주기에 붙는 자동 실행 지점이 훅, 그리고 트리거(trigger)·목표(goal)·검증(verification)·멈추는 규칙(stopping rule)이 다 결합된 실행 구조가 루프입니다.

 

이 다섯 중에서 훅이랑 루프는 겉보기엔 비슷해 보여도 결정적으로 다른 게 하나 있어요. 루프에는 멈추는 규칙이 설계의 일부로 반드시 들어갑니다. “테스트가 다 통과하면 멈춘다”, “최대 5번까지만 재시도한다” 같은 종료 조건이 루프라는 개념 안에 처음부터 박혀 있죠. 스스로 반복하는 구조다 보니, 어디서 멈출지를 안 정하면 루프 자체가 성립을 안 해요.

 

반면 훅에는 그런 게 없어요. 훅은 “이 시점이 오면 이걸 한 번 실행한다”가 전부입니다. 한 번 실행하고 끝나는 게 정상이니까 멈추는 규칙 같은 걸 따로 안 붙이거든요. 그런데 그 훅 안에서 새 세션을 만들어버리면, 새 세션이 다시 같은 훅을 발화시키면서 훅이 사실상 루프처럼 반복되기 시작합니다. 루프의 형태는 갖췄는데 루프의 안전장치인 멈추는 규칙은 없는, 제일 위험한 조합이 돼버리는 거예요.

 

hook과 loop 비교표: hook은 멈추는 규칙 없음(X), loop는 멈추는 규칙이 설계에 반드시 포함(O)
<출처: 작가, GPT로 생성>

 

제 사고가 딱 이거였어요. 훅을 루프처럼 쓰면서 멈추는 규칙을 안 뒀습니다. 종료 훅이 새 세션을 낳고, 그 세션의 종료가 또 새 세션을 낳는 동안, 어디에도 이쯤에서 멈추라는 지시가 없었던 거죠.

 

 

그래서 하지 말아야 할 것들

이 사고 이후 제가 세운 원칙들은 대부분 “하지 말 것”의 모양이에요. 구현 코드를 채우는 것보다 경계선을 확실히 그어두는 게 재발을 막는 데 더 효과가 있더라고요.

 

첫째, 종료 계열 훅에서는 새 세션을 만들지 않아요. SessionEnd처럼 세션이 끝나는 시점에 붙는 훅에서 새 세션을 띄우면, 그 새 세션도 결국 같은 종료 훅을 타게 되거든요. 재귀의 씨앗이 여기 있습니다.

 

둘째, 훅 안에서 LLM을 부르지 않아요. 훅은 로그 남기고, 타임스탬프(timestamp) 찍고, 정적 파일 검사하는 것처럼 결정적이고 짧은 로컬 작업을 할 때 써야 합니다. 반대로 LLM을 부르면 비용이며 시간이며 실패, 재시도, 컨텍스트 로딩이 전부 딸려 와요. 이런 무거운 걸 매 종료 시점마다 자동으로 태우면 통제가 안 됩니다.

 

셋째, 백그라운드 실행으로 통제권을 넘기지 않아요. nohup이랑 &, disown 조합은 프로세스를 사용자 눈에서 떼어내서 지켜보기도, 멈추기도 어렵게 만듭니다. 평범한 스크립트라면 편한 조합이지만, 에이전틱 런타임(agentic runtime) 안에서 새 세션을 이렇게 띄우면 뭔가 잘못됐을 때 손쓸 방법이 사라져요.

 

넷째, LLM이 꼭 필요한 자동화라면 훅이 아니라 명시적인 커맨드(command)나 스킬로 빼요. “세션이 끝나면 알아서 정리해줘”가 아니라 “정리가 필요하면 내가 이 명령을 부른다”로 바꾸는 거죠. 자동 실행과 명시적 실행 사이에 선을 긋는 게 중요합니다.

 

다섯째, 자동화에는 비용 상한을 둬요. 최대 실행 횟수든 쿨다운(cooldown)이든, “무한히 반복되지 않게 막는 장치”가 하나라도 있어야 합니다. 제 훅에는 그게 아예 없었고, 그래서 브레이크 없이 토큰을 60M이나 써버린 거예요.

 

마지막으로, 설정을 바꾼 뒤에는 위험한 신호가 남아 있지 않은지 한 번 훑어봐요. 저는 이런 명령으로 확인합니다.

 

rg 'hooks|SessionEnd|claude -p|nohup|disown' .claude ~/.claude

 

훅 설정에 종료 계열 이벤트가 있는지, 그 안에서 claude를 다시 부르는지, 백그라운드 실행 키워드가 섞여 있는지를 한눈에 볼 수 있어요. settings 파일은 코드만큼, 어쩌면 코드보다 더 조심해서 다뤄야 한다는 걸 이번에 느꼈습니다.

 

 

마치며

AI 에이전트 자동화에서 제일 위험한 건 똑똑한 모델이 아니라 잘못 설계된 루프예요. 모델이 아무리 좋아져도, 멈추는 규칙이 없는 반복 구조 앞에서는 비용이 그대로 폭탄이 됩니다. 편하자고 걸어둔 자동화 하나가 하루 만에 세션 한도를 다 태울 수 있다는 걸, 저는 137,305개의 세션 로그를 지우면서 실감했어요.

 

SessionEnd 같은 종료 계열 훅에서 새 세션이나 LLM을 부르는 자동화, 걸어본 적 있으세요? 있다면 거기에 멈추는 규칙은 두셨나요? 지금 쓰는 설정을 한 번 열어보면 좋겠습니다.


이 글은 AI의 도움을 받아 작성했습니다.

 

ⓒ요즘IT의 모든 콘텐츠는 저작권법의 보호를 받는 바, 무단 전재와 복사, 배포 등을 금합니다.