
Claude Code의 Stop hook으로 자율 에이전트의 폭주를 막는 구현 — 무한 루프 감지 및 세션 강제 종료, 3가지 주의점【2026】
요약
Claude Code의 hooks 기능을 활용하여 자율 에이전트의 무한 루프와 폭주를 방지하는 구현 방법을 다룹니다. PreToolUse를 통한 도구 호출 감지와 Stop hook 사용 시 주의사항을 기술합니다.
핵심 포인트
- 무한 루프 방지를 위해 PreToolUse에서 동일 도구 호출 임계치 감지 필요
- exit code 2를 반환하여 Claude가 다른 접근 방식을 검토하도록 유도
- Stop hook 사용 시 stop_hook_active 필드를 확인하지 않으면 무한 루프 발생 위험
- Stop hook은 도구 호출 도중이 아닌 응답 종료 시점에만 발화함
Claude Code로 코딩 이외의 자율 태스크(장시간 리서치, 다단계 배치 처리, 반복적인 에이전트 운용)를 구성하면서, "정신을 차려보니 같은 도구 호출을 수십 번이나 반복하고 있었다", "세션이 끝나지 않고 API 과금만 늘어나고 있었다"라는 경험이 있는 분들을 위한 기사입니다.
전제 환경:
- Claude Code (2026년 7월 시점의 최신 버전)
- hooks는
.claude/settings.json(또는settings.local.json)의hooks필드에서 설정 - 훅 본체는 Python 3.13으로 기술 (Node.js로도 동일한 구현 가능)
hooks의 기본적인 작성법이나 PreToolUse / PostToolUse를 다뤄본 적이 있는 사람을 상정하고 있으므로, hooks 자체에 대한 입문 부분은 생략합니다.
Stophook은 "에이전트가 응답을 마치려고 하는 순간"에만 발화합니다. 도구 호출을 연달아 수행하는 루프의 도중을 멈추는 용도로는 적합하지 않습니다. - 정말로 폭주(동일 도구 호출의 무한 연발)를 감지하고 싶다면,PreToolUse측에서 최근의 호출을 기록하고, 동일 호출이 임계치를 넘으면 exit code 2로 블록하는 것이 정답입니다. -Stophook을 "태스크가 남아있다면 계속하게 한다"는 용도로 사용할 때는, payload의stop_hook_active를 확인하지 않으면 훅 스스로가 무한히 발화하게 됩니다.
{
"hooks": {
"PreToolUse": [
...
동일 도구·동일 인수의 호출이 연속된 횟수를 /tmp에 기록하고, 임계치를 넘으면 exit code 2로 블록합니다.
# .claude/hooks/loop_guard.py
import json, sys, hashlib, pathlib
STATE = pathlib.Path("/tmp/claude_loop_guard.json")
...
exit code 2를 반환하면 stderr의 내용이 Claude 자신에게 보이는 형태로 피드백되어, 같은 행동을 반복하지 않고 다른 접근 방식을 검토하게 됩니다.
태스크가 남아있다고 판단하면 decision: block을 반환하여 계속하게 하는 구현은 흔하지만, stop_hook_active 체크를 빼면 위험합니다.
# .claude/hooks/force_continue.py
import json, sys
payload = json.load(sys.stdin)
...
일부러 똑같은 파일을 5번 grep 하게 만드는 프롬프트를 던져 테스트했습니다. 1~4회째는 평소대로 실행되었고, 5회째에 loop_guard.py가 exit code 2를 반환하여 stderr 메시지가 대화에 삽입되면서 Claude가 다른 방법(파일을 직접 읽기 등)으로 전환하는 것을 확인할 수 있었습니다. force_continue.py 측도 stop_hook_active를 제거한 상태로 동작시키면 동일한 응답을 계속 반복하는 것을 확인한 후, 체크를 다시 넣었습니다.
Stop hook은 "메인 루프가 응답을 마쳐도 되는가"를 판단하는 타이밍에서만 호출됩니다. 도구 호출을 연발하고 있는 도중에는 발화하지 않기 때문에, "폭주를 막는" 목적으로 Stop hook만 구현하면 영원히 발화하지 않는 훅이 만들어집니다. 루프 자체를 감지하고 싶다면 PreToolUse 측에 판정 로직을 둘 필요가 있습니다.
Stop hook에서 decision: block을 반환하면, Claude는 다시 한번 생각하여 응답하려고 합니다. 하지만 그 응답의 마지막에 다시 Stop hook이 발화합니다. payload에 포함된 stop_hook_active (bool)를 무시하면, "계속하게 함 → 다시 멈추려고 함 → 다시 계속하게 함"의 루프가 물리적으로 끝나지 않게 됩니다. 원인을 파악하기까지 시간이 조금 걸렸던 부분입니다.
인터랙티브 세션이라면 decision: block은 사용자에게 보이는 형태로 대화가 이어지지만, -p
의 headless (headless) 실행에서는 대화 상대가 없기 때문에, 단순히 다음 턴이 자동으로 생성될 뿐이다. 루프 가드(loop guard)를 설정하지 않은 채 headless로 구동하면, 인지하지 못하는 사이에 API 호출 비용만 계속 쌓이게 된다. 장시간의 자율 태스크를 headless로 돌린다면, PreToolUse 측면에서의 폭주 감지는 실질적으로 필수라고 생각하는 것이 좋다.
Claude Code의 훅(hook)은 시그널(signal)이 아니라 JSON / exit code를 통해 프레임워크에 지시를 반환하도록 설계되어 있다. exit code 0은 "그대로 계속 진행", exit code 2는 "차단(block)하고 stderr를 Claude에게 보여줌", 그 외의 exit code는 "에러로 기록하지만 실행은 중단하지 않음"이라는 차이가 있다. 이 3가지 값의 의미를 혼동하면, 원래 중단해야 할 처리가 그대로 통과되거나, 반대로 중단하고 싶지 않은 처리까지 중단되어 버릴 수 있다.
Stop훅은 "중단하려고 한 순간" 전용이다.- 루프 자체의 감지는
PreToolUse측에서 수행한다. stop_hook_active체크를 잊으면, 계속할지 중단할지에 대한 판단이 무한 루프에 빠진다.- headless 모드에서는 "계속"의 의미가 인터랙티브(interactive) 시와 다르므로, 폭주 가드는 필수적이다.
- 자율적으로 태스크를 수행하는 에이전트 운용을 하려면, 우선 어떤 훅이 어느 타이밍에 발화(fire)하는지를 정확히 이해한 후 설계하는 것이 결국 지름길이다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기