DAY 154 · 2026-06-03

拆解 Claude Code 的登入機制

SLIDES · 6
Day 154 slide 1Day 154 slide 2Day 154 slide 3Day 154 slide 4Day 154 slide 5Day 154 slide 6
1 / 6

上一篇寫 cc-switch,提到 Claude Code 會看 ANTHROPIC_AUTH_TOKEN 這個環境變數決定 key 從哪來。但大多數人其實沒設這個變數——你開 Pro/Max 訂閱、跑一次 claude /login 點一下瀏覽器,之後它跑了好幾天還記得你。它怎麼做到的?

如果你也跑 Pro/Max 訂閱、好奇它為什麼幾天不用重登,這篇就是給你的——分析 Claude Code 的訂閱登入:access token(短命的通行證)跟 refresh token(長命的續命卡)各自在做什麼、存在你電腦哪裡、為什麼偶爾它還是會叫你重登一次。

你的登入資訊放在哪裡

三邊都會寫到 ~/.claude/.credentials.json(Windows 路徑是 %USERPROFILE%\.claude\.credentials.json)。Mac 多一層——同一份資料也往系統 Keychain 寫一筆,開 "鑰匙圈存取" app 搜 Claude Code 就能找到。

  • macOS:檔案+Keychain 並存——Keychain 靠作業系統加密保管。
  • Linux:純檔案,權限 600(只有你自己讀得到)。
  • Windows:純檔案,靠 Windows 預設權限限制成只有你能讀。

不管哪一邊,裡面長這樣(欄位名稱,值我抹掉):

{
  "claudeAiOauth": {
    "accessToken": "sk-ant-oat01-...",
    "refreshToken": "sk-ant-ort01-...",
    "expiresAt": 1735000000000,
    "scopes": ["user:file_upload", "user:inference", "user:mcp_servers", "user:profile", "user:sessions:claude_code"]
  }
}

expiresAt 是 Unix 毫秒時間戳。想看人類閱讀格式:

# macOS (BSD date)
date -r $((1735000000000 / 1000))
# Linux (GNU date)
date -d @$((1735000000000 / 1000))

為什麼要分成兩個 token

access token 是真正去打 API 那把鑰匙。每次 Claude Code 送請求,header 裡帶的就是這把。Anthropic 那邊把它設得短命——OAuth 回傳的 expires_in 是 28800 秒,剛好 8 小時。(GitHub 上有人回報更短、一兩小時就被踢,那是 refresh 出包的 bug,不是正常壽命。)短命的好處是:萬一這把外洩,能造成的時間損害有上限。

但短命就有個問題:每幾個小時就叫你重登一次,誰受得了。所以才有 refresh token——它的工作只有一件:拿來去換新的 access token。長命、不直接打 API、靜靜躺在 Keychain 或那個 600 檔裡。

兩個合作的設計就是這樣:真正會在網路上頻繁來回的那把短命;靜靜躺在你電腦上等候差遣的那把長命。 這套是 OAuth 2.0 的標準分工,Google、GitHub、你公司的 SSO 走的都是這套,Claude Code 只是把它套上來。

第一次 /login 那一刻發生了什麼

你跑 claude /login,背後大概是這樣:

  1. Claude Code 自己先想一串很長的亂數(叫 code verifier),把它 hash 一次得到 code challenge。
  2. 開系統瀏覽器,把 code challenge 跟一個 redirect URI(通常是 http://localhost:<port>)一起塞進 https://claude.ai/oauth/authorize?...
  3. 你在瀏覽器登入 Claude 帳號、按授權。
  4. claude.ai 把一個 authorization code 丟回那個 localhost。Claude Code 在背景開的小 server 接到。
  5. CLI 拿這個 code+第一步那串原始亂數,去 token endpoint 換成 access token+refresh token+expiresAt 一組三件。
  6. 寫進 Keychain 或那個 credentials 檔。

中間多一步 "亂數+hash" 叫 PKCE(Proof Key for Code Exchange,唸 pixie),用意是防中間有人偷走 authorization code 也沒用——他沒有最原始那串亂數,換不到 token。Claude Code 是裝在你電腦上的 CLI(OAuth 講的 public client,沒有藏在後端的密鑰可驗身分),這層保護尤其重要。

平常它怎麼幫你續命

每次 Claude Code 啟動或要打 API 前,會先看 expiresAt

  • 還沒到 → 拿 access token 直接打,你沒感覺。
  • 到了或快到了 → 拿 refresh token 去 token endpoint 換一組新的(新 access token、新 expiresAt)。換完寫回 Keychain/檔案,繼續跑。

整段過程透明、你看不到。這也是為什麼平常用幾天還在登入狀態。

但 GitHub issues 上有一票場景會讓這層續命失敗:

  • 機器等待 (sleep) 很久才醒,refresh token 那邊已經被判定失效。
  • 同時開好幾個 Claude Code session,互相搶著換 token,有人拿到舊的就 401。
  • 跑在 headless 機器上(CI、server)、refresh 一掛沒辦法 fallback 開瀏覽器補。
  • 重開機之後,特別是 Windows 上很常見。

失敗的下場都一樣:401 → 叫你 /login 重來一次。這也是為什麼有 claude setup-token 這個指令——它幫你生一個壽命一年的 OAuth token,塞進環境變數 CLAUDE_CODE_OAUTH_TOKEN,CI/server 跑的時候就不用碰瀏覽器。

環境變數一設,整套都跳過

回到上一篇 cc-switch。我那台 Mac 上 Keychain 裡沒有那筆、~/.claude/.credentials.json 也沒生出來——因為我設了 ANTHROPIC_AUTH_TOKEN 指到 MiMo,整套 OAuth 訂閱機制連碰都沒碰。

它的優先順序大概是這樣,從上往下找,第一個有的就用、後面全部跳過:

  1. 雲端供應商變數(Bedrock/Vertex/Foundry)
  2. ANTHROPIC_AUTH_TOKEN(會塞進 Bearer header)
  3. ANTHROPIC_API_KEY(會塞進 X-Api-Key header)
  4. apiKeyHelper 腳本(自訂腳本動態回傳 key,例如從公司 vault 拿短命 token)
  5. CLAUDE_CODE_OAUTH_TOKEN(setup-token 生的那個)
  6. OAuth 訂閱(也就是這篇主角)

等等——AUTH_TOKEN 跟 API_KEY 都是 key,差在哪?差在 Claude Code 把它塞進哪個 HTTP header、原本要給誰看:

  • ANTHROPIC_AUTH_TOKENAuthorization: Bearer <key>。Bearer 是 OAuth/JWT 圈的標準寫法——這篇主角 OAuth 訂閱出去也是塞這個 header,AUTH_TOKEN 等於讓你親自寫第 6 條那個值,直接擋下訂閱那條路。大部分第三方 gateway 也認這個,MiMo 那把 tp-... 走的就是這條。
  • ANTHROPIC_API_KEYX-Api-Key: <key>。Anthropic 自家 API 收 key 用這個 header 名稱,第三方 gateway 大多不會去學。Claude Console 申請的 sk-ant-api03-... 走這條。

設錯邊基本上 401——key 本身是對的,但對方根本沒在看那個 header。兩個同時都設了,AUTH_TOKEN 贏(就是上面這順序)。

所以我切到 MiMo 那一刻、ANTHROPIC_AUTH_TOKEN 一設下去,Claude Code 連我訂閱的 access token 還沒過期都不去看——它走第 2 條路了。把那個變數拿掉,它才會回去走第 6 條、讀 Keychain/credentials 檔。

cc-switch 幫你切設定組,背後就是在第 2、3、5、6 條這幾個 token 來源之間切(第 4 條 apiKeyHelper 是動態腳本,cc-switch 不管那層)。


一句話總結:access token 短命、refresh token 長命,兩個合作下來你感覺從來沒被登出——直到那個 refresh token 也死了的那一天,才會叫你 /login 重來。

Sources:

延伸閱讀
看完整 165 篇 →