Skip to content

OAuth2 authentication for claude-sdk-cli#180

Merged
shellicar merged 9 commits intomainfrom
feature/oauth2-login
Apr 6, 2026
Merged

OAuth2 authentication for claude-sdk-cli#180
shellicar merged 9 commits intomainfrom
feature/oauth2-login

Conversation

@shellicar
Copy link
Copy Markdown
Owner

@shellicar shellicar commented Apr 6, 2026

Summary

Replaces the static ANTHROPIC_API_KEY environment variable with a full OAuth2 PKCE flow against the Claude.ai authorisation server.

What changed

Auth flow

  • AnthropicAuth class handles the full PKCE flow: build auth URL, open browser, wait for local callback, exchange code for tokens
  • getCredentials() is the single entry point — triggers login if no credentials, refreshes if expired
  • fetchProfile() called after first login to store subscriptionType and rateLimitTier
  • Credentials stored at ~/.claude/.credentials.json
  • loadCredentials / saveCredentials validate through authCredentials Zod schema

Token injection

  • AnthropicAgentOptions.apiKey: stringauthToken: () => Promise<string> (per-request getter, enables transparent refresh)
  • TokenRefreshingAnthropic subclasses the Anthropic SDK client and overrides bearerAuth() and apiKeyAuth() — called per-request before validateHeaders
  • ApiKeySetter (() => Promise<string>) is declared in the SDK types but the runtime discards non-string values in the constructor (this.apiKey = typeof apiKey === 'string' ? apiKey : null). The subclass fixes this.
  • SDK internal types (NullableHeaders, buildHeaders, FinalRequestOptions, etc.) re-implemented in sdkInternals.ts — those paths are not in the package exports map. Linting disabled on that file. Mirrored from @anthropic-ai/sdk@0.82.0.

Build

  • Entry point moved from src/main.tssrc/entry/main.ts (matches build entryPoints glob)
  • minify tied to !watch, shebang banner added
  • dropLabels wired

Notes

  • sdkInternals.ts should be verified against SDK source if @anthropic-ai/sdk is upgraded

…n getter

- AnthropicAgentOptions.apiKey replaced with authToken: () => Promise<string>
- customFetch injects Authorization: Bearer <token> header per-request,
  enabling transparent token refresh without SDK changes
- AnthropicAgent passes authToken: '-' to satisfy SDK null-check; real token
  comes through fetch layer
- AnthropicAuth.getCredentials() called at startup; triggers OAuth flow if no
  credentials, refreshes if expired
- fetchProfile() called after first login to store subscriptionType and
  rateLimitTier in credentials
- credentialsPath() moved to ~/.claude/.credentials.json (was ./credentials.json
  next to the binary)
- loadCredentials/saveCredentials now validate through authCredentials Zod schema
- AuthCredentials type gains subscriptionType and rateLimitTier fields
- AuthorisationUrl updated to claude.com/cai/oauth/authorize
- claude-cli src/main.ts moved to src/entry/main.ts to match entryPoints glob
- Build improvements: dropLabels wired, minify tied to !watch, shebang banner
  added, splitting enabled
- Compaction trigger threshold bumped 125k -> 150k tokens
…hropic subclass

ApiKeySetter is a type lie in @anthropic-ai/sdk@0.82.0 — the constructor
discards non-string values (typeof apiKey === 'string' ? apiKey : null)
so the function is never called. authToken is string-only.

Instead, subclass Anthropic and override the protected bearerAuth() method
which is called per-request before validateHeaders. Returns a plain
Record<string,string> which buildHeaders accepts via Object.entries —
no imports from @anthropic-ai/sdk/internal/* needed.

customFetch is now logging-only again.
Mirror of @anthropic-ai/sdk/internal/* types and buildHeaders — those paths
are not in the package exports map so they cannot be imported directly.

sdkInternals.ts is intentionally written to match SDK source conventions
not our own; linting is disabled on it via biome.json overrides.

TokenRefreshingAnthropic.ts imports from sdkInternals and stays clean.
@shellicar shellicar added this to the 1.0 milestone Apr 6, 2026
@shellicar shellicar added the enhancement New feature or request label Apr 6, 2026
@shellicar shellicar self-assigned this Apr 6, 2026
@shellicar shellicar added the enhancement New feature or request label Apr 6, 2026
@shellicar shellicar requested a review from bananabot9000 April 6, 2026 05:03
@shellicar shellicar changed the title feat: OAuth2 authentication for claude-sdk-cli OAuth2 authentication for claude-sdk-cli Apr 6, 2026
Copy link
Copy Markdown
Collaborator

@bananabot9000 bananabot9000 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean decomposition. 19 Auth files, each one job. The commit progression tells the story: customFetch injection -> discovered ApiKeySetter is a type lie -> subclass with bearerAuth/apiKeyAuth override -> extract sdkInternals.

Observations:

TokenRefreshingAnthropic - The approach of capturing the getter before super() drops it is solid. The SDK constructor doing typeof apiKey === 'string' ? apiKey : null while declaring ApiKeySetter = () => Promise<string> in the types is exactly the kind of thing that wastes hours. Good that you documented it in the JSDoc.

sdkInternals.ts - The SDK version comment + biome override is the right call. buildHeaders is a faithful mirror including the branded symbol pattern. The upgrade verification checklist in the header comment is good future-proofing.

waitForCallback - server.close() and resolve/reject happen in the same handler. If the browser hits /callback twice (double-click, prefetch), second request hits a closing server. Low risk but noted.

refreshCredentials - parseTokenResponse blanks subscriptionType/rateLimitTier on refresh (empty strings from the token response). Profile data from the original login is lost. getCredentials could carry forward the profile fields from the pre-refresh credentials, or re-fetch profile on refresh.

compactInputTokens - Nice addition. The null trigger when not specified lets the server decide defaults. The 125k->150k bump in the hardcoded call site matches the compaction threshold bump in the commit message.

pnpm override @anthropic-ai/sdk@>=0.79.0 <0.81.0: >=0.81.0 - Agent SDK 0.2.92 was pulling 0.74.0 as a dep? The override forces it to 0.82.0 which is what your sdkInternals mirrors. Smart.

execFile('open', ...) - macOS only. Fine for now since that's the target, just noting it for when linux/WSL becomes relevant.

Build changes (splitting, dropLabels, shebang, minify tied to !watch) are consistent across both cli and sdk-cli. Good hygiene.

LGTM.

@shellicar shellicar merged commit 2d8454b into main Apr 6, 2026
4 checks passed
@shellicar shellicar deleted the feature/oauth2-login branch April 6, 2026 05:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants