Linux サーバで Claude Code のアカウントを切り替える — 利用上限で止まらないバッチ基盤

AWS
B!

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.jsonuserID で見分けられないかと考えましたが、対照実験で否定
されました。同じトークンを別々の HOME で実行すると、値が変わります。

main 試行1: a1b2c3d4e5f60718293a4b5c6d7e8f90...
main 試行2: 0f9e8d7c6b5a4938271605f4e3d2c1b0...   ← 同じトークンなのに違う

インストールごとの乱数で、アカウントの識別には使えません。
対照実験をしていなければ「値が違う=別アカウント」と誤断していました。

結局「ログインしたときにどちらのアカウントを選んだか」を人が覚えておくしかありません。
同じアカウントで2本発行してしまっていた場合、切り替えても上限は解けません。

落とし穴まとめ

macOS 版security コマンド依存。Linux では動かない
トークンの渡し方引数だと ps に見える。標準入力で受ける
2本目の取得シークレットウィンドウで開かないと同じアカウントのトークンが出る
自動切り替え自分自身を判定から除外しないと無限に回る
登録1件のみrotate は何もしない。2本目を入れるまで従来どおり上限で止まる
別アカウント判定サーバ側からは確認できないuserID は乱数で使えない

まとめ

サーバ側の切り替えは、Keychain を使う macOS 版よりむしろ単純でした。
長命トークンはただの文字列なので、スロットに置いて差し替えるだけです。

そして自動化の価値は「速くなる」ことではなく、朝起きたときに止まっていないことです。
上限は必ず来ます。来たときに人の手を介さず次へ進める設計にしておくと、
バッチ基盤の実効稼働率が目に見えて変わります。

シリーズ記事

Mac の定期バッチを AWS へ移した一連の記録です。インフラを作る → その上で Claude Code を動かす → 利用上限で止まらないようにする、の順に読むと流れがつながります。

  1. CloudFormation で Lightsail を立ち上げる
    月12ドル固定のバッチ実行基盤をコードで作る
  2. AWS 上で Claude Code を cron から動かす
    サブスク認証のまま、API 課金なしで
  3. Linux サーバで Claude Code のアカウントを切り替える(この記事)
    利用上限で止まらないバッチ基盤

B!
← 一覧へ戻る