← ブログに戻る

複数アカウントで気づいた Claude Code の2ファイル認証 (credentials.json + ~/.claude.json)

Claude Code の Max プランと Team プランを、1台のマシンで合わせて3アカウント運用したいと思いました。5時間ウィンドウを時差でずらせば連続作業時間を伸ばせるからです。

素朴に ~/.claude/.credentials.json を上書きすればアカウントを切り替えられるはず、と思ってスクリプトを書いたところ、症状が2つ出ました。

  • /status が古いアカウントを表示し続ける
  • しかし API 認証は新しい token で通っている

見た目と実態がズレたまま動く、というのが一番厄介です。Claude Code はどこを見て /status を描画しているのか。ここを調べたら、Claude Code の認証は2ファイル管理だと分かりました。この記事は、その発見と、発見を元に作った切替ツール claude-shift の話です。

なぜ複数アカウントを1台で回したかったのか

Claude Code の5時間ウィンドウは「時計時刻の00

」ではなく「初回リクエスト起点」です。アカウントAを10時に使い始めたら、そのアカウントの枠は15時にリセットされます。アカウントBを15時に使い始めたら、Bの枠は20時にリセット。3アカウントを時差でずらせば、単純計算で15時間分の作業枠が連なります。

私が保有しているのは以下の3つです (以降、記事内では account-a / account-b / account-c と表記します)。

  • account-a (Team、Org-X 所属)
  • account-b (Team、同じ Org-X 所属)
  • account-c (Max、個人契約)

このうち Max の account-c は個人契約なので週次上限があり、平日の日中に食い切ります。Team 側は組織単位で予算枠がゆるいので、Max が枯れたら Team へフォールバックしたい。この運用を Claude Code の CLI で回すには、認証を切り替える薄いラッパーがあれば足ります。

症状: /status が変わらない、でも認証は通る

最初に書いたラッパーは、以下のような流れでした。

# 保存済みの credentials を差し替える
cp ~/.claude-shift/accounts/account-b.json ~/.claude/.credentials.json

これで新しい token が有効になるので、Claude Code の API 呼び出しは account-b の枠で通ります。実際に claude "usage 確認" を打つと、account-b 側の課金として動作します。

ところが /status を叩くと表示は前のアカウントのままです。同一セッション内だけの話ならセッション再起動で追従するはず、と思って claude を落として立ち上げ直しました。それでも表示は前のアカウントを保持したままです。認証は切り替わっているのに、表示だけが古い状態に張り付いていました。

これが実運用で困る理由は、/status を信じてトークン残量を判断するからです。3アカウントを行き来している最中に「今どのアカウントで動いているか」が表示から読めないと、意図せず Max 枠を食い潰したり、Team 側で無駄に消費したりします。

ハズレ仮説を1つ潰した

最初に立てた仮説は「credentials.json のフォーマットが壊れていて、Claude Code がフォールバック表示している」でした。JSON を検査したところ、フォーマットは正しく、claudeAiOauth.accessToken / refreshToken の構造も一致します。API 側で認証が通っていることが、この仮説を消しました。フォーマットが壊れていたら 401 が返ってきているはずです。

次に「別プロセスが古い状態をキャッシュしている」を疑って、Claude Code 関連のプロセスを全部落として立て直しました。表示は変わりません。「表示の更新頻度が低いだけで、いずれ追従する」も疑って15分ほど放置しました。追従しません。

ここまで来て、そもそも表示の情報源が credentials.json ではないのでは、と考え始めました。

実測: profile API で3アカウントが別ユーザーだと確認

先に足元を固めておく必要があります。私が保存している3アカウントは、本当に別ユーザーなのでしょうか。全部同じユーザーで、単にラベル違いで保存しているだけなら、そもそも問題は起きません。

Anthropic の OAuth profile API を叩けば確認できます。Claude Code 内部でもこの API は使われていて、必要な header は次の2つです。

  • Authorization: Bearer <accessToken>
  • anthropic-beta: oauth-2025-04-20

3アカウントの token でそれぞれ叩いた結果は次のとおりです (email / 名前 / 組織名は本記事用に匿名化しています)。

// account-a
{"account": {"email": "alice@example.com", "full_name": "Alice"},
 "organization": {"name": "Org-X", "organization_type": "claude_team"}}

// account-b (同一 org、別 user)
{"account": {"email": "bob@example.com", "full_name": "Bob"},
 "organization": {"name": "Org-X", "organization_type": "claude_team"}}

// account-c (個人 Max)
{"account": {"email": "carol@example.com", "full_name": "Carol"},
 "organization": {"organization_type": "claude_max"}}

email も full_name も別で、organization_type も Team / Team / Max と分かれています。3アカウントは本当に別ユーザーです。同じ organization の中に別ユーザーが 2 人並ぶケース (account-a と account-b) と、独立組織のケース (account-c) を対比できます。

発見: ~/.claude.json の oauthAccount

3アカウントが別ユーザーだと確認できたので、次はどこに表示情報が保存されているかを探しました。~/.claude/ 配下を全部見ても credentials.json 以外にそれらしいファイルは見つかりません。範囲を広げて ~/ 直下を眺めたところ、~/.claude.json というファイルがありました。

開くと oauthAccount というフィールドがあります。中身は profile API の応答とほぼ同じ構造で、email、full_name、organization などが入っていました。

// ~/.claude.json (抜粋、値は匿名化)
{
  "oauthAccount": {
    "emailAddress": "alice@example.com",
    "organizationName": "Org-X",
    "organizationRole": "admin",
    "workspaceRole": null,
    "organizationType": "claude_team"
  },
  ...
}

/status が読んでいるのはこちらです。credentials.json は token 保管専用で、表示情報は ~/.claude.jsonoauthAccount フィールドが単一の情報源になっていました。

言い換えると、Claude Code の認証は次の2ファイル構成です。

  • ~/.claude/.credentials.json: token 保管 (access / refresh)
  • ~/.claude.json: oauthAccount フィールドが /status の表示ソース

credentials.json だけ差し替えると、認証は新 token で通るのに /status~/.claude.json の古いスナップショットを表示し続けます。認証と表示が別のファイルで管理されていて、片側だけ書き換えると齟齬が残る、という仕様でした。

検証: 両ファイル書き換え → /status も追従

仮説を確定させるために、両ファイルを一貫して書き換えてから /status を確認しました。

  1. credentials.json を account-b の内容で上書き
  2. ~/.claude.jsonoauthAccount を profile API から取り直した内容で置き換え
  3. claude を再起動して /status を確認

/status の表示が account-b に切り替わりました。これで認証と表示が一致します。逆に片方だけ書き換えると必ず食い違うことも、両方向で確認しています。

~/.claude.json にはこれ以外にも会話履歴のポインタや UI 設定が入っているので、oauthAccount フィールドだけを差し替える必要があります。ファイル全体を上書きすると、他のセッション状態を壊します。

ツール化: shift use の内部

ここまでの発見を1コマンドで回すために claude-shift を書きました。中核は shift use <name> で、内部は次の3ステップです。

// cli/accounts.js (抜粋)
export async function switchAccount(name, opts = {}) {
  // 1. 現在アクティブなアカウントを検出し、
  //    ディスク上の credentials.json (refresh 済みの可能性がある) を
  //    そのアカウントのスロットへ sync back する
  const active = getActiveAccount(accountsDir, credentialsPath);
  if (active && active !== name) {
    writeFileSync(
      join(accountsDir, `${active}.json`),
      readFileSync(credentialsPath)
    );
  }

  // 2. 新しいアカウントの credentials で credentials.json を上書き
  writeFileSync(credentialsPath, readFileSync(target));
  chmodSync(credentialsPath, 0o600);

  // 3. 新しい token で profile API を叩いて oauthAccount を取り、
  //    ~/.claude.json の該当フィールドだけを書き換える
  const token = extractToken(readJsonSafe(target));
  const profile = await fetchProfile(token);
  writeOAuthAccountToClaudeJson(
    profileToOAuthAccount(profile),
    claudeJsonPath
  );
}

3ステップ目の書き換えは ~/.claude.json を JSON としてパースし、oauthAccount キーだけ差し替えて書き戻す形にしています。他のフィールドには触りません。

step 1 の sync back を入れているのは、Claude Code はセッション中に token を refresh するからです。差し替え前の credentials.json はディスク上で refresh されていることがあり、単純に上書きするとその refresh 分を捨ててしまいます。次にそのアカウントに戻したときに 401 が発生して再ログインが必要になったりします。sync back で refresh 済みの状態を元スロットに保存してから上書きすれば、この破損を防げます。

副産物: 5時間ウィンドウを能動的に起動する

2ファイル認証の話が本題ですが、複数アカウント運用を実装したことで副産物もいくつか出ました。

shift seed <name> は、指定アカウントで最小サイズの課金リクエストを1発だけ打つコマンドです。目的は5時間ウィンドウを能動的に起動することです。深夜のうちに shift seed account-b を回しておけば、その5時間ウィンドウは深夜起点になり、翌朝の作業開始時点で数時間分の枠が既に消化された状態になります。翌日の後半に新しい枠が回ってくるように、意図的にずらせます。

Chrome 拡張 も付けました。ローカル API サーバー (127.0.0.1:19867) がポーリング間隔ごとに /api/oauth/usage を叩いて SQLite に保存し、Chrome 拡張の popup が全アカウントの5時間枠と週次の使用率、リセット時刻、アクティブアカウントを表示します。分析モーダルは 6h / 24h / 7d の3アカウント横断 SVG 折れ線グラフを描き、popup から 切替 ボタンで active を差し替えることもできます。read-only ではなく、切替と観測を同じ popup に載せています。

学び: ドキュメントに載っていない挙動を実測で潰す

この件の教訓は、/status の情報源をドキュメントで確認できなかったところに集約されます。Claude Code の公式 docs には ~/.claude.jsonoauthAccount フィールドについての記述はなく、私が見つけたのはローカルファイルの grep からでした。

LLM 系ツールは公開仕様が薄い領域が多く、実挙動と仕様書の間に落差が残ります。落差の存在自体は許容範囲ですが、その落差に張り付いた表示バグは実運用で判断を誤らせます。私のケースだと、/status を信じて Max 枠を使い切ってしまう、といった実害が起きうる場所でした。

対処は3段階に分けられます。第一段階は、公式 API を実測して自分の仮定を潰すこと。今回は profile API で「3アカウントが別ユーザー」を先に確定させたことで、「同じユーザーが表示されているだけ」の可能性を除外できました。第二段階は、表示と実態のズレを検出したときに情報源を疑うこと。credentials.json 以外の場所に情報源があると気づいたのはこの段階です。第三段階は、発見をツールに閉じ込めて再発を防ぐこと。shift use の中で2ファイル同期を自動化すれば、次回以降このズレは踏まなくて済みます。

claude-shift の内部仕様は docs/knowledge/claude-code-auth-internals.md にまとめてあります。同じ問題を踏んだ人が調べたときに、grep 一発で答えに辿り着けるようにしておきたかったためです。

Claude Code の公式仕様に変更が入って oauthAccount の位置が動いたら、この記事も追記します。当面は2ファイル管理を前提に運用する予定です。