OAuth2 authentication for claude-sdk-cli#180
Conversation
…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.
bananabot9000
left a comment
There was a problem hiding this comment.
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.
Summary
Replaces the static
ANTHROPIC_API_KEYenvironment variable with a full OAuth2 PKCE flow against the Claude.ai authorisation server.What changed
Auth flow
AnthropicAuthclass handles the full PKCE flow: build auth URL, open browser, wait for local callback, exchange code for tokensgetCredentials()is the single entry point — triggers login if no credentials, refreshes if expiredfetchProfile()called after first login to storesubscriptionTypeandrateLimitTier~/.claude/.credentials.jsonloadCredentials/saveCredentialsvalidate throughauthCredentialsZod schemaToken injection
AnthropicAgentOptions.apiKey: string→authToken: () => Promise<string>(per-request getter, enables transparent refresh)TokenRefreshingAnthropicsubclasses the Anthropic SDK client and overridesbearerAuth()andapiKeyAuth()— called per-request beforevalidateHeadersApiKeySetter(() => 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.NullableHeaders,buildHeaders,FinalRequestOptions, etc.) re-implemented insdkInternals.ts— those paths are not in the package exports map. Linting disabled on that file. Mirrored from@anthropic-ai/sdk@0.82.0.Build
src/main.ts→src/entry/main.ts(matches build entryPoints glob)minifytied to!watch, shebang banner addeddropLabelswiredNotes
sdkInternals.tsshould be verified against SDK source if@anthropic-ai/sdkis upgraded