배경
Claude Code를 여러 quota로 사용하는 경우가 있습니다. 예를 들면 다음과 같습니다.
-
Max plan을 2개를 구독해서 번갈아 가면서 쓰는 경우
-
같은 이메일 계정에 여러 organization이 연결되어 각각 별도의 quota가 있는 경우
-
Team plan과 Enterprise plan을 동시에 구독하는 경우
이런 상황에서 한쪽 quota가 소진되면 다른 쪽으로 전환해서 쓸 수 있습니다. 문제는 quota A가 소진되어 quota B로 전환한 뒤, quota A가 초기화되는 시점에 다시 돌아오는 걸 잊기 쉽다는 것입니다. Claude Code의 Hook 기능을 활용하면 이 전환을 자동화할 수 있습니다.
이 글은 Claude Code에게 프롬프트를 내려서 hook 스크립트를 생성하고, 생성된 코드를 분석하는 방식으로 구성됩니다.
전체 구성
필요 환경
-
Claude Code v2.1.78(2026년 3월 18일 릴리즈) 이상 (
StopFailurehook 이벤트 지원) -
jq설치 필요
원리: Quota 소진 시 Claude Code의 동작
사용자에게 보여주는 메시지
Claude Code에서는 API quota를 소진 시 초기화 예정 시간을 포함한 메시지를 안내합니다.
You've hit your limit · resets 2pm (Asia/Seoul)
주간 한도 소진 시에는 날짜도 포함됩니다:
You've hit your limit · resets Feb 20, 5pm (Asia/Seoul)
StopFailure hook 이벤트
Quota 소진 시 발생하는 hook 이벤트는 StopFailure 입니다 (2026년 3월 18일 릴리즈된 v2.1.78에서 도입). Notification 이나 Stop 은 quota 소진 시 발생하지 않습니다.
StopFailure 이벤트의 JSON 입력 (실제 기록된 데이터){
"session_id": "5cbd3730-...",
"transcript_path": "/home/user/.claude/projects/.../abc123.jsonl",
"cwd": "/home/user/project",
"hook_event_name": "StopFailure",
"error": "rate_limit",
"last_assistant_message": "You've hit your limit · resets 11pm (Asia/Seoul)"
}
실행 흐름
아래는 quota 전환 자동화의 전체 흐름입니다. 스크립트 3개와 hook 2개로 구성됩니다.
[Quota A 소진]
│
▼
StopFailure hook (rate_limit) ──→ on-rate-limit.sh
│ │
│ last_assistant_message에서 초기화 시간 파싱
│ │
│ ▼
│ quota-switch-reset-time 파일에 기록
│
▼
[사용자가 /logout 후 Quota B로 로그인]
│
▼
[Quota B로 작업 계속]
│
▼
[초기화 시간 경과 후 새 세션 or /clear]
│
▼
SessionStart hook ──→ check-quota-switch.sh
│
시간 비교: 현재 >= 초기화 시간?
│
Yes ──┤
│
credentials 삭제 + 세션 블로킹
│
▼
[Claude Code 재시작 → Quota A로 로그인]
| 스크립트 | 역할 | 호출 시점 |
|---|---|---|
|
Hook 입력의 |
|
|
초기화 시간이 지났으면 credentials를 삭제하여 재로그인 유도 |
|
|
초기화 시간을 수동으로 기록 (자동 파싱 실패 시 fallback) |
사용자가 직접 실행 |
구현과 테스트 프롬프트
구현 프롬프트
아래와 같은 프롬프트로 Claude Code에게 구현을 요청합니다.
Claude Code를 quota A, quota B 두 개로 쓰고 있어. Quota A가 소진되면 quota B로 전환해서 쓰다가, quota A의 초기화 시간이 지난 후 새 세션이나 /clear 시에 자동으로 quota B를 logout시켜서 quota A로 복귀하고 싶어. 아래 요구사항대로 구현해줘.
quota 소진 감지: StopFailure hook에 rate_limit matcher를 걸어서 소진 시점을 감지해줘. 스크립트 이름은 on-rate-limit.sh로 해줘.
초기화 시간 자동 기록: on-rate-limit.sh에서 hook 입력의 last_assistant_message 필드에서 "resets TIME (TIMEZONE)" 패턴을 파싱하여 초기화 시간을 ~/.claude/quota-switch-reset-time에 epoch으로 저장해줘.
초기화 시간 수동 기록(fallback): 자동 파싱이 실패할 경우를 대비해 record-quota-reset.sh도 만들어줘. "4h", "30m" 같은 상대 시간과 절대 시간을 지원해야해.
자동 복귀: SessionStart hook에서 check-quota-switch.sh를 실행해서, 현재 시간이 초기화 시간을 지났으면 credentials를 삭제하여 재로그인을 유도해줘. startup과 clear 두 matcher 모두 등록해줘.
Claude Code는 이 요청을 받아 세 개의 스크립트 파일을 생성하고, `~/.claude/settings.json`에 hook 설정까지 직접 등록했습니다. 사용자가 `settings.json`을 수동으로 편집할 필요 없이, 프롬프트 하나로 스크립트 작성과 hook 등록이 모두 완료됩니다.
테스트 프롬프트
실제 quota를 소진하지 않고도 hook과 스크립트가 정상 동작하는지 검증할 수 있습니다. 테스트 역시 Claude Code에게 요청하면 됩니다.
~/.claude/settings.json에 등록된 on-rate-limit.sh와 check-quota-switch.sh를 실제 quota 소진 없이 테스트하고 싶어. on-rate-limit.sh는 hook JSON 입력을 직접 구성해서 stdin으로 넘기는 방식으로 테스트해줘. "resets 2pm (Asia/Seoul)" 같은 5시간 한도 메시지와 "resets Feb 20, 5pm (Asia/Seoul)" 같은 주간 한도 메시지 두 가지를 모두 테스트해줘. 각 테스트 후 hook-events.log와 quota-switch-reset-time 파일을 확인해줘. check-quota-switch.sh는 과거 시간의 epoch을 기록한 뒤 실행해서 credentials 삭제 동작을 검증해줘. 단, 실제 credentials가 삭제되므로 테스트 전에 백업하고 테스트 후 복원해줘.
Claude Code는 이 요청에 따라 hook JSON을 직접 구성하여 `on-rate-limit.sh`에 전달하고, 결과를 확인하는 과정을 수행합니다. 예를 들어 5시간 한도 메시지 테스트는 다음과 같이 실행됩니다:
echo '{
"hook_event_name": "StopFailure",
"error": "rate_limit",
"last_assistant_message": "You'\''ve hit your limit · resets 2pm (Asia/Seoul)"
}' | ~/.claude/scripts/on-rate-limit.sh
테스트가 성공하면 ~/.claude/hook-events.log 에 "Auto-recorded quota reset time"이 기록되고, ~/.claude/quota-switch-reset-time 파일에 epoch 값이 저장됩니다.
사용 방법
1단계: quota A 소진
Quota A가 소진되면 StopFailure hook이 자동으로 on-rate-limit.sh 를 실행합니다. Hook 입력의 last_assistant_message 에서 초기화 시간을 파싱하여 `~/.claude/quota-switch-reset-time`에 기록합니다. `~/.claude/hook-events.log`에서 "Auto-recorded quota reset time"이 기록되었는지 확인할 수 있습니다.
자동 파싱에 실패한 경우, 로그에 실패 사유가 기록됩니다. 이때는 수동으로 쿼터가 초기화될 시간을 기록합니다:
# 절대 시간
~/.claude/scripts/record-quota-reset.sh "2026-03-29 09:00"
# 상대 시간
~/.claude/scripts/record-quota-reset.sh 4h
~/.claude/scripts/record-quota-reset.sh 30m
2단계: quota B로 전환
Claude Code에서 /logout 을 실행한 후 quota B 계정으로 재로그인합니다.
3단계: 자동 복귀
기록된 초기화 시간이 지난 후에 Claude Code를 새로 시작하거나 /clear 를 실행하면 SessionStart hook이 동작합니다. Quota B의 credentials가 자동으로 제거되고, Claude Code를 재시작하면 quota A로 로그인할 수 있습니다.
생성된 코드 분석
아래 코드는 Linux 환경에서 생성된 것입니다. macOS에서의 차이점은 macOS에서의 차이점를 참고하세요.
Quota 소진 감지 및 초기화 시간 자동 기록
~/.claude/scripts/on-rate-limit.sh 는 StopFailure hook에서 실행되는 스크립트입니다. Quota 소진 시 hook 입력의 last_assistant_message 필드에서 초기화 시간을 파싱하여 기록합니다.
#!/bin/bash
# StopFailure hook (matcher: rate_limit)
# 1. Log the event for diagnostics
# 2. Extract reset time from last_assistant_message (hook JSON input)
LOG_FILE="$HOME/.claude/hook-events.log"
INPUT=$(cat)
# Step 1: Log the event
echo "=== $(date '+%Y-%m-%d %H:%M:%S') ===" >> "$LOG_FILE"
echo "$INPUT" | jq . >> "$LOG_FILE" 2>/dev/null || echo "$INPUT" >> "$LOG_FILE"
echo "" >> "$LOG_FILE"
# Step 2: Extract reset time from last_assistant_message
LAST_MSG=$(echo "$INPUT" | jq -r '.last_assistant_message // empty')
if [ -z "$LAST_MSG" ]; then
echo "No last_assistant_message in hook input" >> "$LOG_FILE"
exit 0
fi
RESET_LINE=$(echo "$LAST_MSG" | grep -oP \
'resets?\s+(at\s+)?(\w+\s+\d+,?\s+)?\d{1,2}(:\d{2})?\s*[ap]m(\s*\([^)]+\))?')
if [ -z "$RESET_LINE" ]; then
echo "No reset time found in message: $LAST_MSG" >> "$LOG_FILE"
exit 0
fi
echo "Found reset info: $RESET_LINE" >> "$LOG_FILE"
# Step 3: Parse the reset time
TIME_PART=$(echo "$RESET_LINE" | grep -oP '\d{1,2}(:\d{2})?\s*[ap]m')
DATE_PART=$(echo "$RESET_LINE" | grep -oP '(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+\d{1,2}' | head -1)
TZ_PART=$(echo "$RESET_LINE" | grep -oP '\(([^)]+)\)' | tr -d '()')
# Convert 12-hour to 24-hour
HOUR=$(echo "$TIME_PART" | grep -oP '^\d{1,2}')
MINUTE=$(echo "$TIME_PART" | grep -oP ':\K\d{2}' || echo "00")
AMPM=$(echo "$TIME_PART" | grep -oP '[ap]m')
if [ "$AMPM" = "pm" ] && [ "$HOUR" -ne 12 ]; then
HOUR=$((HOUR + 12))
elif [ "$AMPM" = "am" ] && [ "$HOUR" -eq 12 ]; then
HOUR=0
fi
TIME_24=$(printf "%02d:%s" "$HOUR" "$MINUTE")
# Build datetime string
TODAY=$(date "+%Y-%m-%d")
if [ -n "$DATE_PART" ]; then
YEAR=$(date "+%Y")
DATETIME="$DATE_PART $YEAR $TIME_24"
else
DATETIME="$TODAY $TIME_24"
fi
# Convert to epoch
if [ -n "$TZ_PART" ]; then
RESET_EPOCH=$(TZ="$TZ_PART" date -d "$DATETIME" +%s 2>/dev/null)
else
RESET_EPOCH=$(date -d "$DATETIME" +%s 2>/dev/null)
fi
if [ -z "$RESET_EPOCH" ]; then
echo "Failed to parse reset time: $DATETIME (TZ=$TZ_PART)" >> "$LOG_FILE"
exit 0
fi
# If parsed time is in the past, assume next day (5-hour limit case)
NOW_EPOCH=$(date +%s)
if [ "$RESET_EPOCH" -le "$NOW_EPOCH" ] && [ -z "$DATE_PART" ]; then
RESET_EPOCH=$((RESET_EPOCH + 86400))
fi
# Step 4: Record the reset time
RESET_FILE="$HOME/.claude/quota-switch-reset-time"
echo "$RESET_EPOCH" > "$RESET_FILE"
RESET_DISPLAY=$(date -d "@$RESET_EPOCH" "+%Y-%m-%d %H:%M:%S")
echo "Auto-recorded quota reset time: $RESET_DISPLAY" >> "$LOG_FILE"
exit 0
last_assistant_message 필드에서 resets TIME (TIMEZONE) 패턴을 grep하여 초기화 시간을 추출하는 원리는 StopFailure hook 이벤트를 참고하세요.
초기화 시간 수동 기록 (fallback)
~/.claude/scripts/record-quota-reset.sh 는 자동 파싱이 실패했을 때 초기화 시간을 수동으로 기록하는 스크립트입니다.
#!/bin/bash
# Record when quota A resets so the hook can switch back from quota B
# Usage: record-quota-reset.sh "2026-03-29 09:00"
# record-quota-reset.sh 4h (4 hours from now)
# record-quota-reset.sh 30m (30 minutes from now)
RESET_FILE="$HOME/.claude/quota-switch-reset-time"
if [ -z "$1" ]; then
echo "Usage: $0 <datetime or duration>"
echo " Examples:"
echo " $0 '2026-03-29 09:00' # absolute time"
echo " $0 4h # 4 hours from now"
echo " $0 30m # 30 minutes from now"
exit 1
fi
INPUT="$1"
# Parse duration format (e.g., 4h, 30m)
if [[ "$INPUT" =~ ^([0-9]+)h$ ]]; then
HOURS="${BASH_REMATCH[1]}"
RESET_EPOCH=$(date -d "+${HOURS} hours" +%s)
RESET_DISPLAY=$(date -d "+${HOURS} hours" "+%Y-%m-%d %H:%M:%S")
elif [[ "$INPUT" =~ ^([0-9]+)m$ ]]; then
MINS="${BASH_REMATCH[1]}"
RESET_EPOCH=$(date -d "+${MINS} minutes" +%s)
RESET_DISPLAY=$(date -d "+${MINS} minutes" "+%Y-%m-%d %H:%M:%S")
else
# Absolute datetime
RESET_EPOCH=$(date -d "$INPUT" +%s 2>/dev/null)
if [ $? -ne 0 ]; then
echo "Error: Cannot parse datetime '$INPUT'"
exit 1
fi
RESET_DISPLAY=$(date -d "$INPUT" "+%Y-%m-%d %H:%M:%S")
fi
echo "$RESET_EPOCH" > "$RESET_FILE"
echo "Quota A reset time recorded: $RESET_DISPLAY"
echo "Saved to: $RESET_FILE"
echo ""
echo "Now run /logout in Claude Code and log in with quota B."
echo "When the reset time arrives, the next session start or /clear will auto-logout quota B."
절대 시간("2026-03-29 09:00")과 상대 시간(4h, 30m) 두 가지 형식을 지원합니다. 내부적으로는 Unix epoch 타임스탬프로 변환하여 ~/.claude/quota-switch-reset-time 파일에 저장합니다.
자동 복귀
~/.claude/scripts/check-quota-switch.sh 는 SessionStart hook에서 실행되는 스크립트입니다.
#!/bin/bash
# SessionStart hook: check if quota A has reset
# If so, remove credentials to force re-login with quota A
RESET_FILE="$HOME/.claude/quota-switch-reset-time"
CRED_FILE="$HOME/.claude/.credentials.json"
# No reset time recorded, nothing to do
if [ ! -f "$RESET_FILE" ]; then
exit 0
fi
RESET_EPOCH=$(cat "$RESET_FILE")
NOW_EPOCH=$(date +%s)
if [ "$NOW_EPOCH" -ge "$RESET_EPOCH" ]; then
# Time to switch back to quota A
RESET_DISPLAY=$(date -d "@$RESET_EPOCH" "+%Y-%m-%d %H:%M:%S")
# Backup quota B credentials and remove current credentials
if [ -f "$CRED_FILE" ]; then
cp "$CRED_FILE" "$HOME/.claude/.credentials.quota-b.bak"
rm -f "$CRED_FILE"
fi
# Remove reset time file
rm -f "$RESET_FILE"
# Exit with code 2 to block session and show message
echo "Quota A has reset (reset time: $RESET_DISPLAY)." >&2
echo "Quota B credentials have been removed." >&2
echo "Please restart Claude Code to log in with quota A." >&2
exit 2
fi
# Not yet time to switch
exit 0
이 스크립트의 핵심 동작은 다음과 같습니다.
-
기록된 초기화 시간 파일이 없으면 `exit 0`으로 아무 동작 없이 종료합니다. 평소에는 hook이 세션 시작을 방해하지 않습니다.
-
현재 시간이 초기화 시간 이상이면 전환을 실행합니다.
-
~/.claude/.credentials.json을~/.claude/.credentials.quota-b.bak으로 백업한 후 삭제합니다. Credentials 파일이 없으면 Claude Code가 다음 시작 시 로그인을 요청합니다. -
`exit 2`로 세션을 블로킹하고 stderr로 안내 메시지를 출력합니다. Claude Code hook에서 exit code 2는 세션 진행을 차단하고 stderr 메시지를 사용자에게 보여주는 약속된 규칙입니다.
settings.json hook 설정
~/.claude/settings.json 에 StopFailure`와 `SessionStart hook을 등록합니다.
{
"hooks": {
"StopFailure": [
{
"matcher": "rate_limit",
"hooks": [
{
"type": "command",
"command": "/home/user/.claude/scripts/on-rate-limit.sh"
}
]
}
],
"SessionStart": [
{
"matcher": "startup",
"hooks": [
{
"type": "command",
"command": "/home/user/.claude/scripts/check-quota-switch.sh"
}
]
},
{
"matcher": "clear",
"hooks": [
{
"type": "command",
"command": "/home/user/.claude/scripts/check-quota-switch.sh"
}
]
}
]
}
}
"matcher": "rate_limit" 으로 quota 소진 이벤트만 필터링합니다. SessionStart 이벤트는 startup (새 세션 시작)과 clear (/clear 실행) 두 경우 모두 같은 스크립트를 실행하도록 설정했습니다.
한계
-
credentials 저장소 의존: Linux에서는
~/.claude/.credentials.json파일을, macOS에서는 Keychain을 직접 조작하는 방식이므로, Claude Code의 내부 인증 구조가 변경되면 동작하지 않을 수 있습니다. -
완전 자동 로그인은 불가: Credentials 제거 후 재시작 시 로그인 화면이 나타나지만, quota A 계정을 자동으로 선택해주지는 않습니다. 사용자가 직접 quota A 계정으로 로그인해야 합니다.
-
세션 중간 전환은 지원하지 않음:
SessionStarthook은 새 세션 시작이나/clear시에만 동작합니다. 세션 중간에 초기화 시간이 지나더라도 현재 세션에서는 전환이 일어나지 않습니다. 매 프롬프트마다 체크하는 방식(UserPromptSubmithook)도 가능하지만, 세션 중간에 credentials가 제거되면 진행 중인 작업과 컨텍스트가 유실되므로 권장하지 않습니다. -
메시지 형식 의존:
on-rate-limit.sh`의 파싱은 `last_assistant_message필드의resets TIME (TIMEZONE)패턴에 의존합니다. Claude Code 버전에 따라 메시지 형식이 달라져 왔으므로, 향후 형식이 변경되면 파싱이 실패할 수 있습니다. 실패 시~/.claude/hook-events.log에 원인이 기록되며, `record-quota-reset.sh`로 수동 기록할 수 있습니다.
macOS에서의 차이점
Claude Code는 실행 환경의 OS를 인식합니다. macOS에서 같은 요청을 하면 date 명령어 부분이 BSD 문법으로 생성됩니다. 주요 차이는 다음과 같습니다.
| 용도 | Linux (GNU date) | macOS (BSD date) |
|---|---|---|
상대 시간 (4시간 후) |
|
|
절대 시간 파싱 |
|
|
epoch → 표시 형식 |
|
|
이 외에도 macOS에서는 다음 차이가 있습니다:
-
grep -oP(PCRE)가 macOS 기본 grep에서 지원되지 않습니다.brew install grep후 `ggrep`을 사용하거나, 정규식을 POSIX 호환으로 변경해야 합니다. -
macOS에서 Claude Code의 credentials는 macOS Keychain에 저장됩니다.
~/.claude/.credentials.json파일이 존재하지 않으므로,check-quota-switch.sh에서 파일을 삭제하는 방식이 동작하지 않습니다. macOS에서 프롬프트를 내리면security delete-generic-password명령으로 Keychain에서 credentials를 삭제하는 스크립트가 생성됩니다.
Twitter
Facebook
Reddit
LinkedIn
Email