Claude のプランには利用上限があります。夜間バッチが上限に当たると、そこから朝まで
何も進みません。手元の Mac では2アカウントを手で切り替えて凌いでいましたが、
その仕組みは macOS Keychain 前提で、サーバへ持っていけませんでした。
Linux 用に作り直したので、設計と落とし穴を共有します。
利用上限を検知したら自動で切り替わるところまで含みます。
記事中の IP アドレス・ホスト名・トークンはすべてダミーです。
この記事は3本シリーズの 3 本目です。
あわせて読む: CloudFormation で Lightsail を立ち上げる/AWS 上で Claude Code を cron から動かす
macOS 版がそのまま使えない理由
macOS 版は、Keychain に保存された認証情報を複製・書き戻すことで切り替えていました。
# macOS: Keychain のサービス名を複製して保管する
security find-generic-password -s "Claude Code-credentials" -a "$ACCT" -w
security add-generic-password -U -s "Claude Code-credentials::sub" -a "$ACCT" -w "$SECRET"
security コマンドは macOS 固有なので、Linux では動きません。
サーバ側はむしろ単純
サーバでは claude setup-token が発行する長命トークン(sk-ant-oat01-…)で
認証しています。これはアカウントごとに1本発行できる、ただの文字列です。
つまり 2本持って差し替えるだけ。Keychain より素直でした。
~/.claude/accounts/
├── main.token (600)
├── sub.token (600)
└── active ← いまどちらを使っているか
secrets.sh は「いま有効なスロットを読む」形にしておきます。
# ~/.claude/secrets.sh (切り替えツールが書く。手で編集しない)
if [ -r "$HOME/.claude/accounts/main.token" ]; then
export CLAUDE_CODE_OAUTH_TOKEN="$(cat "$HOME/.claude/accounts/main.token")"
fi
cron からは BASH_ENV でこのファイルを読ませます。
切り替えは次のバッチから効くので、実行中のジョブを壊しません。
SHELL=/bin/bash
BASH_ENV=/home/ubuntu/.claude/secrets.sh
PATH=/home/ubuntu/.local/bin:/usr/local/bin:/usr/bin:/bin
切り替えツール
#!/bin/bash
# Claude のアカウント(=OAuthトークン)を切り替える。
#
# acct list 保存済みと現在の状態
# acct add <名前> トークンを標準入力から受け取って保存
# acct use <名前> 切り替える
# acct current いま有効な名前
# acct rotate 次のアカウントへ回す(利用上限の自動復旧用)
set -u
DIR="$HOME/.claude/accounts"
ACTIVE="$DIR/active"
SECRETS="$HOME/.claude/secrets.sh"
umask 077
mkdir -p "$DIR"; chmod 700 "$DIR"
die() { echo "$*" >&2; exit 1; }
slot_path() { printf '%s/%s.token' "$DIR" "$1"; }
current_name() { [ -f "$ACTIVE" ] && cat "$ACTIVE" || echo ""; }
names() { ls -1 "$DIR"/*.token 2>/dev/null | sed 's#.*/##; s#\.token$##'; }
# 有効なスロットを指すように secrets.sh を書き直す
write_secrets() {
local name="$1"
cat > "$SECRETS" <<EOF
# 切り替えツールが書く。手で編集しない。
# いま有効なアカウント: ${name}
if [ -r "$DIR/${name}.token" ]; then
export CLAUDE_CODE_OAUTH_TOKEN="\$(cat "$DIR/${name}.token")"
fi
EOF
chmod 600 "$SECRETS"
}
case "${1:-list}" in
list)
cur="$(current_name)"
for n in $(names); do
[ "$n" = "$cur" ] && printf '* ' || printf ' '
printf '%-10s %s文字\n' "$n" "$(wc -c < "$(slot_path "$n")" | tr -d ' ')"
done
echo "現在: ${cur:-未設定}"
;;
add)
name="${2:-}"
[ -n "$name" ] || die "使い方: acct add <名前> (トークンは標準入力から)"
case "$name" in *[!a-zA-Z0-9_-]*) die "名前は英数字・ハイフン・アンダースコアだけ";; esac
# ★引数では受け取らない。ps に見えてしまうため。
read -r tok
[ -n "$tok" ] || die "トークンが空"
case "$tok" in sk-ant-oat01-*) ;; *) die "setup-token の出力を渡すこと";; esac
printf '%s' "$tok" > "$(slot_path "$name")"
chmod 600 "$(slot_path "$name")"
echo "保存した: $name(${#tok} 文字)"
[ -f "$ACTIVE" ] || { printf '%s' "$name" > "$ACTIVE"; write_secrets "$name"; }
;;
use)
name="${2:-}"
[ -f "$(slot_path "$name")" ] || die "そんなアカウントは無い: $name"
printf '%s' "$name" > "$ACTIVE"; chmod 600 "$ACTIVE"
write_secrets "$name"
echo "切り替えた: $name"
;;
current) current_name ;;
rotate)
cur="$(current_name)"
all="$(names)"
cnt="$(printf '%s\n' "$all" | grep -c . || true)"
if [ "${cnt:-0}" -lt 2 ]; then
echo "切り替え先が無い(登録 ${cnt:-0} 件)。何もしない。"; exit 0
fi
next="$(printf '%s\n' "$all" | grep -A1 -x "$cur" | tail -1)"
[ -n "$next" ] && [ "$next" != "$cur" ] || next="$(printf '%s\n' "$all" | head -1)"
printf '%s' "$next" > "$ACTIVE"; chmod 600 "$ACTIVE"
write_secrets "$next"
echo "$cur -> $next へ回した"
;;
*) die "不明なコマンド(list / add / use / current / rotate)" ;;
esac
トークンは引数で渡さない
add が標準入力から読むのは意図的です。引数で渡すと、実行中の一瞬とはいえps で他のユーザーから見えます。
ssh batch-runner 'acct add sub'
# ここでトークンを貼り付けて Enter → Ctrl-D
sk-ant-oat01- で始まるかも検証しています。間違えて別の文字列を入れると、
夜中に全バッチが認証エラーで落ちるだけで、原因が分かりにくいからです。
2つ目のアカウントを登録する
ssh -t batch-runner 'claude setup-token'
★ブラウザはシークレットウィンドウで開くこと。通常ウィンドウだと1つ目の
アカウントのセッションが使い回され、同じアカウントのトークンがもう1本出てきます。
$ ssh batch-runner 'acct list'
* main 108文字
sub 108文字
現在: main
add は既存の有効アカウントを変えません。登録しただけでは切り替わりません。
自動切り替え
ここが本題です。バッチの実行結果を記録する共通スクリプトの末尾に、
利用上限を検知したらアカウントを回す処理を足しました。
# --- 利用上限に当たっていたらアカウントを回す ---
# ★報告を送ったあとに行う。記録には「上限に当たった側」の結果が残り、
# 次の回から別アカウントで動く。
if [ "$BATCH" != "account-switch" ] \
&& command -v acct >/dev/null 2>&1 \
&& grep -qiE "hit your limit|usage limit|利用上限|out of extra usage" "$LOG" 2>/dev/null; then
BEFORE="$(acct current)"
RESULT="$(acct rotate 2>&1)"
echo "利用上限を検知したのでアカウントを切り替えた: ${RESULT}" | tee -a "$LOG"
AFTER="$(acct current)"
if [ "$BEFORE" != "$AFTER" ]; then
# 切り替えたこと自体も記録に残す
SW="$(mktemp)"
echo "利用上限のため ${BEFORE} -> ${AFTER} へ切り替え(きっかけ: ${BATCH})" > "$SW"
NOW="$(date +%s)"
bash "$0" account-switch "$SW" 0 "$NOW" "$NOW" >/dev/null 2>&1
rm -f "$SW"
fi
fi
★自分自身を除外しないと無限に回る
最初の版にはバグがありました。切り替えを記録するために自分自身を呼び直している
のですが、その通知の文面に「利用上限」が入っています。
利用上限のため main -> sub へ切り替え(きっかけ: job-a)
^^^^^^^^ これが判定条件に引っかかる
つまり「切り替えた → 通知を記録 → その通知に『利用上限』がある → また切り替える」
と延々と回り続けます。書いた直後に気づいて、1行のガードを入れました。
if [ "$BATCH" != "account-switch" ] && ...
再帰する仕組みを書いたら、出口の条件を先に書く。 当たり前のようでいて、
「記録も残したい」という善意の追加で簡単に踏みます。
切り替えたことを記録に残す意味
管理画面の実行ログに account-switch という行が残るようにしました。
これがないと、後日ログを見返したときに「なぜ途中から別アカウントで動いているのか」
が分からなくなります。自動で直る仕組みほど、直したことを記録すべきです。
検証:偽のスロットで往復させる
本物のトークンを2本使う前に、形式だけ合わせたダミーで動作を確かめました。
$ printf "sk-ant-oat01-DUMMY-FOR-ROTATE-TEST\n" | acct add subtest
$ acct list
* main 108文字
subtest 34文字
現在: main
$ acct rotate
main -> subtest へ回した
$ grep -o "accounts/[a-z]*\.token" ~/.claude/secrets.sh
accounts/subtest.token # ← 参照先も切り替わっている
$ acct use main && rm -f ~/.claude/accounts/subtest.token
secrets.sh の参照先が実際に変わっているかまで見るのが要点です。
「切り替えた」と表示されても、読む側が変わっていなければ意味がありません。
確認できなかったこと
正直に書いておきます。2つのトークンが本当に別アカウントのものか、サーバ側から
判定する方法が見つかりませんでした。
claude auth status は所属を返しません。
{
"loggedIn": true,
"authMethod": "oauth_token",
"apiProvider": "firstParty"
}
~/.claude.json の userID で見分けられないかと考えましたが、対照実験で否定
されました。同じトークンを別々の HOME で実行すると、値が変わります。
main 試行1: a1b2c3d4e5f60718293a4b5c6d7e8f90...
main 試行2: 0f9e8d7c6b5a4938271605f4e3d2c1b0... ← 同じトークンなのに違う
インストールごとの乱数で、アカウントの識別には使えません。
対照実験をしていなければ「値が違う=別アカウント」と誤断していました。
結局「ログインしたときにどちらのアカウントを選んだか」を人が覚えておくしかありません。
同じアカウントで2本発行してしまっていた場合、切り替えても上限は解けません。
落とし穴まとめ
| macOS 版 | security コマンド依存。Linux では動かない |
| トークンの渡し方 | 引数だと ps に見える。標準入力で受ける |
| 2本目の取得 | シークレットウィンドウで開かないと同じアカウントのトークンが出る |
| 自動切り替え | 自分自身を判定から除外しないと無限に回る |
| 登録1件のみ | rotate は何もしない。2本目を入れるまで従来どおり上限で止まる |
| 別アカウント判定 | サーバ側からは確認できない。userID は乱数で使えない |
まとめ
サーバ側の切り替えは、Keychain を使う macOS 版よりむしろ単純でした。
長命トークンはただの文字列なので、スロットに置いて差し替えるだけです。
そして自動化の価値は「速くなる」ことではなく、朝起きたときに止まっていないことです。
上限は必ず来ます。来たときに人の手を介さず次へ進める設計にしておくと、
バッチ基盤の実効稼働率が目に見えて変わります。
シリーズ記事
Mac の定期バッチを AWS へ移した一連の記録です。インフラを作る → その上で Claude Code を動かす → 利用上限で止まらないようにする、の順に読むと流れがつながります。
- CloudFormation で Lightsail を立ち上げる
月12ドル固定のバッチ実行基盤をコードで作る - AWS 上で Claude Code を cron から動かす
サブスク認証のまま、API 課金なしで - Linux サーバで Claude Code のアカウントを切り替える(この記事)
利用上限で止まらないバッチ基盤
Analyzegear
