클로드 코드에서 세션을 닫을 때, AI가 할 일 목록을 알아서 정리해주면 편하겠다 싶었어요. 그 아이디어 하나 때문에 토큰을 60M 넘게 태우고 세션 한도(session limit)를 통째로 날려먹었습니다.
무슨 상황이었냐면요. 전 클로드 코드에서 어떤 작업이 끝나면 그 결과에 맞춰 다른 폴더의 작업 현황을 동기화하는 자동화를 걸어뒀습니다. 그런데 그 자동화로 종료 시점에 새 클로드 코드 세션을 하나 띄우게 돼 있었어요. 이 새로운 세션이 끝나면서 같은 자동화를 다시 탔고, 그게 또 새로운 세션을 띄웠습니다. 종료가 종료를 낳는 구조였던 거죠. 그래서 미친듯이 토큰이 낭비된 겁니다.
이 글은 그 사고를 처음부터 복기한 기록이에요. 뭔가 대단한 버그를 잡은 이야기는 아니에요. 오히려 며칠 동안 로그를 한참 뒤져서야 겨우 진범을 찾은, 좀 창피한 축에 가까운 이야기입니다. 그래도 에이전틱 하네스(agentic harness)를 쓰다 보면 한 번쯤 마주칠 함정이라, 경험을 겪은 그대로 공유하는 게 도움이 되겠다 싶어 정리했습니다.

사고를 알아차린 건 2026년 7월 3일 오전이었어요. 그날 시킨 작업 자체는 별거 없었습니다. 개인 폴더에 루틴이랑 메모 몇 개 만들어달라는, 몇 분이면 끝날 일이었죠. 그런데 잠깐 뒤에 사용량을 보니까 토큰이 비정상적으로 올라가 있었고, 곧 세션 한도에 걸리더라고요.
이럴 때 보통 가장 최근에 바꾼 걸 먼저 의심하잖아요. 저도 그랬습니다. 짚이는 변경이 두 개 있었거든요.
하나는 설치 방식이었어요. 얼마 전에 홈브루(Homebrew)로 관리하던 클로드 코드를 지우고 npm install -g로 다시 깔았거든요. 재설치하다 뭔가 꼬였나 싶었죠. 다른 하나는 모델이었습니다. 그 무렵 Sonnet 5이 새로 나와 쓰고 있었는데, 새 모델이 예상보다 토큰을 많이 먹나 하는 의심이 들더라고요.
둘 다 그럴듯했는데, 둘 다 틀렸어요. 설치 방식이든 모델이든, 작은 작업 하나가 그 정도 토큰을 쓸 이유가 안 됩니다. 작업은 작은데 사용량은 크다는 이상함이 결국 저를 다른 쪽으로 돌려세웠어요. 제가 시킨 작업이 원인이 아니라, 그 작업이 끝난 다음에 벌어지는 뭔가에 있었던 거죠.
방향을 바꾸고 제일 먼저 열어본 건 프로젝트의 .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가 실행된다.
문제는 이 순환이 멈출 이유가 코드 어디에도 없었다는 거예요.

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

세션 로그도 확인했어요. 클로드 코드가 세션마다 남기는 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차 효과 같습니다. 용량 자체보다 짧은 시간에 작은 실패 세션이 수천 개 생긴 패턴이 시스템 전반을 흔든 거죠.
이 사고의 본질은 결국 훅을 루프(loop)처럼 써버린 데 있었다고 생각합니다.
에이전틱 하네스를 다룰 때 저는 이걸 프롬프트, 컨텍스트, 도구(tool), 훅, 루프 다섯으로 나눠서 봐요. 모델에 주는 지시가 프롬프트, 모델이 참고하는 환경이 컨텍스트, 모델이 쓰는 능력이 도구, 하네스의 생명주기에 붙는 자동 실행 지점이 훅, 그리고 트리거(trigger)·목표(goal)·검증(verification)·멈추는 규칙(stopping rule)이 다 결합된 실행 구조가 루프입니다.
이 다섯 중에서 훅이랑 루프는 겉보기엔 비슷해 보여도 결정적으로 다른 게 하나 있어요. 루프에는 멈추는 규칙이 설계의 일부로 반드시 들어갑니다. “테스트가 다 통과하면 멈춘다”, “최대 5번까지만 재시도한다” 같은 종료 조건이 루프라는 개념 안에 처음부터 박혀 있죠. 스스로 반복하는 구조다 보니, 어디서 멈출지를 안 정하면 루프 자체가 성립을 안 해요.
반면 훅에는 그런 게 없어요. 훅은 “이 시점이 오면 이걸 한 번 실행한다”가 전부입니다. 한 번 실행하고 끝나는 게 정상이니까 멈추는 규칙 같은 걸 따로 안 붙이거든요. 그런데 그 훅 안에서 새 세션을 만들어버리면, 새 세션이 다시 같은 훅을 발화시키면서 훅이 사실상 루프처럼 반복되기 시작합니다. 루프의 형태는 갖췄는데 루프의 안전장치인 멈추는 규칙은 없는, 제일 위험한 조합이 돼버리는 거예요.

제 사고가 딱 이거였어요. 훅을 루프처럼 쓰면서 멈추는 규칙을 안 뒀습니다. 종료 훅이 새 세션을 낳고, 그 세션의 종료가 또 새 세션을 낳는 동안, 어디에도 이쯤에서 멈추라는 지시가 없었던 거죠.
이 사고 이후 제가 세운 원칙들은 대부분 “하지 말 것”의 모양이에요. 구현 코드를 채우는 것보다 경계선을 확실히 그어두는 게 재발을 막는 데 더 효과가 있더라고요.
첫째, 종료 계열 훅에서는 새 세션을 만들지 않아요. 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의 모든 콘텐츠는 저작권법의 보호를 받는 바, 무단 전재와 복사, 배포 등을 금합니다.