
Claude Code를 무인으로 자율 개선시키기 ― 비용으로 폭주를 막는 autopilot
요약
Claude Code를 사용자 입력 없이 자율적으로 개선하는 autopilot 시스템 구축 방법을 다룹니다. 비용 폭주와 무한 루프를 방지하기 위해 토큰 잔량 수치 기반의 예산 관리, 실행 모델 가변 설정, 타임아웃 적용 등의 제어 메커니즘을 설명합니다.
핵심 포인트
- 비용 폭주 방지를 위해 라벨이 아닌 실제 토큰 잔량 수치로 실행 여부 결정
- 잔량에 따라 모델 종류와 최대 턴(Turn) 수를 가변적으로 조절하여 효율화
- 무한 루프 방지를 위해 턴 수 제한 외에 벽시계 타임아웃(Wall-clock timeout) 적용
- launchd를 활용해 매일 정해진 시간에 헤드리스 모드로 태스크 수행
「Claude Code 환경」 시리즈의 제8탄입니다. 대화 로그를 장기 기억으로 바꾸는 이야기에 이어 무인 작업을 소개했었는데요, 이번에는 그 우두머리 ―― 사용자 입력 없이 Claude Code가 자신의 환경을 계속해서 개선하게 만드는 autopilot에 관한 이야기입니다.
「자율 루프 (Autonomous Loop)」라고 하면 듣기에는 좋지만, 무인으로 LLM을 돌리면 두 가지 무서운 일이 일어납니다. ① 플랜(Plan) 할당량을 다 써버린다 와 ② 같은 태스크를 끝없이 반복한다. 실제로 저는 두 가지 모두 겪었습니다. 이 기사는 그 두 가지를 어떻게 기계적으로 멈추게 했는지에 대한 기록입니다.
launchd를 통해 매일 아침 5:00에 기동하여, claude -p를 헤드리스(Headless)로 「개선 태스크를 하나 집어 들어, 1 Phase만 진행하고 종료한다」를 반복합니다.
- 대상은
~/.claude/하위 디렉터리만 (개인 프로젝트나 Vault의 개인 정보에는 접근하지 않음) dry(프롬프트 생성만) /apply/once의 3가지 모드- 실행하기 전에 비용 할당량을 체크하고, 할당량이 위험하면 즉시 skip
무인으로 「똑똑한 일을 조금씩」 하게 만드는 것이 목표이며, 한 번에 대규모 개조를 시키지는 않습니다.
첫 번째 게이트는 비용입니다. 5시간 블록(Block)의 출력 토큰을 보고, 위험 수위라면 실행 전에 멈춥니다.
BUDGET=$(~/.claude/scripts/token-budget-advisor.sh --short)
if echo "$BUDGET" | grep -qE '🔴|critical|cap-near'; then
log "ABORT: budget critical"; exit 0
...
하지만 라벨 판정만으로는 허점이 있었습니다. 🟡burst 표시 상태임에도 실제 잔량이 마이너스인 경우가 그대로 통과되어, 잔량 -8003인 상태에서 무거운 모델이 606초 동안 실행된 적이 있습니다. 그래서 라벨과는 별개로, 잔량을 수치로 재계산하여 0 이하이면 실행하지 않습니다.
REMAINING=$((800000 - BLOCK_OUT)) # 5h block cap 800k 가정
if [ "$REMAINING" -le 0 ]; then
log "SKIP: block exhausted"; exit 0
...
비용 상한은 「라벨」이 아니라 「잔량의 수치」로 관리해야 했습니다. 색상이나 문자열 임계값 판정은 경계선에서 반드시 빠져나갑니다. 마지막에는 뺄셈을 통해 <= 0 인지를 확인하는 것이 확실합니다.
나아가 잔량에 따라 effort와 max-turns를 가변적으로 조절합니다. 잔량이 적으면 경량·적은 턴(Turn)으로, 여유가 있으면 높게 설정합니다. 무인 작업의 기본 모델은 저렴한 것으로 설정했습니다 (고성능 모델을 상용하여 5시간 블록을 다 써버리고, 이후의 모든 슬롯이 SKIP이 되었던 사고에 대한 반성입니다). 무거운 태스크를 의도적으로 돌릴 때만 환경 변수로 덮어씁니다.
MODEL="${AUTOPILOT_MODEL:-claude-sonnet-4-6}" # 기본은 저렴한 모델
턴 수만으로는 멈추지 않기 때문에, **벽시계 타임아웃 (Wall-clock timeout)**도 적용합니다. max-turns는 턴 수만 제한하므로, 7.25시간 동안 실행된 실적이 있었기에 gtimeout 7200 (2h)으로 상한을 둡니다.
이것이 가장 효과적이었던 수정입니다. 태스크 후보를 next-session-todo.md의 「High impact」 섹션에서 가져오는데, 처음의 awk 명령이 고장 나 있어서 후보가 항상 비어 있었습니다.
# 구 구현의 범위 패턴 /^### High impact/,/^###/ 은
# 시작 행 자체가 종료 조건에도 일치하여 즉시 종료 → 항상 비어 있음.
# → 폴백(Fallback)으로 설정된 고정 태스크가 매번 선택되어, 동일 태스크를 6일 동안 17회 반복.
후보가 비어 있으면 폴백 고정 태스크가 매번 선택되어, 같은 태스크를 6일 동안 17회 수행하고 있었습니다. 범위 패턴을 플래그(Flag) 방식으로 수정하여 후보를 올바르게 열거하고, 추가로 이력 기반의 중복 제거(Dedupe)를 추가했습니다.
- 최근 48시간 내에
exit 0으로 완료된 태스크는 skip - 최근 2회 연속으로 실패한 태스크도 skip (불가능한 것을 계속 시도하지 않도록 함)
if any(r.get("exit_code") == 0 for r in recent):
print("done-recently") # 최근에 수행함 → 다음 후보로
if len(recent) >= 2 and all(r["exit_code"] != 0 for r in recent[-2:]):
...
이력은 1행 1레코드의 JSONL 형식으로 유지하며, 태스크명(task name)·종료 코드(exit code)·소요 시간(seconds)·모델(model)·노력(effort)을 기록합니다. 이를 통해 "최근에 수행한 작업" 또는 "계속해서 막혀 있는 작업"을 기계적으로 판정할 수 있습니다.
무인으로 실행하면, claude -p 명령의 "N MB를 회수했습니다"라는 AI의 자기 신고(self-report)를 아무도 검증하지 않는다는 문제가 발생합니다. 따라서 harness 측에서 실제 측정값을 가져와 결과 파일에 병기합니다.
DISK_BEFORE_KB=$(du -sk "$HOME/.claude" | awk '{print $1}')
# ...claude -p 실행...
DISK_AFTER_KB=$(du -sk "$HOME/.claude" | awk '{print $1}')
...
결과 파일의 끝에 "## 자기 검증 (harness 실측 / Claude의 주장 아님)"이라는 항목으로 디스크 차이(disk delta)와 변경된 파일 수를 기록하고, "본문의 수치 주장이 이 실측값과 괴리가 있다면 본문을 의심하라"는 문구를 덧붙입니다. AI가 말하는 숫자와 OS가 측정한 숫자를 반드시 나란히 배치하는 것. 이것이 무인 운용의 신뢰성을 확보하는 핵심이었습니다.
폭주를 막으려면 인간 측에서도 비용을 상시 확인할 수 있어야 합니다. Claude Code의 상태 표시줄(status line)에 5시간/7일 플랜 사용률을 표시하고 있습니다. 기쁜 점은, 이를 stdin에서 직접 가져올 수 있다는 것입니다. 인증이나 엔드포인트 호출도 필요 없습니다.
H5=$(j '.rate_limits.five_hour.used_percentage')
H5R=$(j '.rate_limits.five_hour.resets_at')
D7=$(j '.rate_limits.seven_day.used_percentage')
...
rate_limits는 "구독 후 첫 번째 API 응답 후에만 나타나기" 때문에, 첫 번째 턴까지는 --로 폴백(fallback)합니다. 이를 통해 "🕐 5h 42% ⏪14:30 / 📅 7d 18%"와 같이 플랜 잔량과 리셋 시각을 항상 확인할 수 있습니다. autopilot이 백그라운드에서 소모한 분량도 여기에 즉시 반영됩니다.
라벨 판정만으로는 잔량 마이너스 상태를 통과함 → 잔량을 수치로 재계산하여 <= 0일 때 중단
max-turns 설정만으로는 7시간 동안 실행됨 → gtimeout으로 실제 시간(wall-clock time) 캡(cap) 적용
고성능 모델 상용으로 플랜을 다 써버려 전체 스킵 발생 → 무인 실행 기본값은 저렴한 모델로 설정, 무거운 작업 시에만 덮어쓰기
awk의 범위 패턴이 비어 있어 고정 태스크를 17회 반복 → 플래그(flag) 방식 + 이력 중복 제거(dedupe)
AI의 자기 신고를 아무도 검증하지 않음 → harness에서 실측하여 결과에 병기
launchd의 최소 PATH 설정으로 claude를 찾지 못함 → PATH → .local/bin → nvm 순으로 폴백
무인 루프는
비용 한도를 수치로 먼저 체크한 뒤 실행합니다 (라벨 판정은 경계값에서 누락될 수 있음).
폭주는
잔량 캡(cap) · 실제 시간 타임아웃(timeout) · 저렴한 모델 기본값이라는 삼중 장치로 막습니다.
동일 태스크 반복은
이력 JSONL의 중복 제거(dedupe) (최근 완료/연속 실패 건 스킵)로 해결합니다.
AI의 자기 신고는 반드시 harness 실측값과 병기하여, 과장이나 오독을 탐지할 수 있게 합니다.
플랜 한도는
statusline의 stdin에서 가져옵니다. 상시 시각화가 모니터링의 토대입니다.
지금까지 총 8개의 글을 통해 기억(memory)·기술(skill)·컨텍스트(context)·launchd·협업(collaboration)·보안(security)·장기 기억(long-term memory)·자율 루프(autonomous loop) 등 제 Claude Code 환경의 거의 전체를 다루었습니다. 읽어주셔서 감사합니다.
Lily(@bokuwalily)― 개인 개발자. Claude Code로 자동화 기반을 구축하며 iOS 앱과 웹 서비스를 양산하고 있습니다.
- 제작한 앱은 포트폴리오(portfolio)에 정리해 두었습니다 📱
- 최신 소식 및 개발 뒷이야기는 X @bokuwalily에서 발신하고 있습니다 🌍
- OSS: github.com/bokuwalily 🐙
여러분의 ❤️와 공유는 큰 힘이 됩니다!
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기