fix(claude): never send Anthropic subscription tokens to a third-party endpoint - #84
Open
pofallon wants to merge 1 commit into
Open
fix(claude): never send Anthropic subscription tokens to a third-party endpoint#84pofallon wants to merge 1 commit into
pofallon wants to merge 1 commit into
Conversation
…y endpoint `ANTHROPIC_BASE_URL` is the documented way to aim the Claude Agent SDK at an Anthropic-compatible vendor. The `anthropic` SDK reads that variable from the environment on its own, so airframe's direct calls (`list_models`, `count_tokens`) already followed it off-site — but the auth ladder had no `ANTHROPIC_AUTH_TOKEN` rung, which is the credential those vendors tell you to export. A user following a vendor's setup instructions therefore fell straight through to `~/.claude/.credentials.json` and POSTed their Anthropic subscription OAuth token to that vendor. Silent, no error, indistinguishable from success. Split the credentials into two classes: - Anthropic-minted subscription tokens (`CLAUDE_CODE_OAUTH_TOKEN`, `~/.claude/.credentials.json`) — withheld unless the resolved base URL is Anthropic's own API. They authenticate the user's Anthropic account and are worthless to any other vendor except as a stolen secret. - Endpoint credentials (explicit `api_key=`, `ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN`) — sent wherever the caller aimed the adapter, since compatible vendors reuse exactly these slots. Adds `_is_anthropic_endpoint()` and folds the two duplicated auth ladders into a shared `_resolve_anthropic_auth()`; the duplication note in `count_tokens` no longer holds now that the logic is non-trivial. A third-party base URL with no endpoint credential now raises `RuntimeAuthError` explaining the withholding rather than falling back. Only airframe's own direct HTTP calls are affected. The `claude` CLI subprocess still does its own auth resolution from the inherited environment, which is what makes pointing the adapter at a compatible endpoint work at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UBTr6Q6kTGUMQiwwBHcdMj
This was referenced Aug 23, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
ANTHROPIC_BASE_URLis the documented way to aim the Claude Agent SDK at an Anthropic-compatible vendor endpoint. TheanthropicSDK reads that variable from env on its own, soClaudeCodeRuntime.list_models()and.count_tokens()already followed it off-site.But the auth ladder had no
ANTHROPIC_AUTH_TOKENrung — which is exactly the credential those vendors tell you to export. So the ladder fell through to~/.claude/.credentials.jsonand sent the user's Anthropic subscription OAuth token to the third party. Silent; no error; looks like it worked.Reproduced before the fix:
After:
The rule
Credentials split into two classes:
CLAUDE_CODE_OAUTH_TOKEN,~/.claude/.credentials.jsonapi_key=,ANTHROPIC_API_KEY,ANTHROPIC_AUTH_TOKENSubscription tokens authenticate the user's own Anthropic account and are worthless to another vendor except as a stolen secret. The API-key-shaped slots are precisely what compatible vendors reuse for their own credentials, so those stay unscoped — scoping them would break the legitimate use case.
A third-party base URL with no endpoint credential now raises
RuntimeAuthErrornaming the withheld sources, rather than quietly falling back.Changes
_is_anthropic_endpoint()— hostname check, conservative on unparseable input (unparseable → third-party, so the failure mode is a withheld token rather than a leaked one)._resolve_anthropic_auth()— folds the two duplicated ladders into one. The old "duplication is readable" note incount_tokensno longer holds now that the logic branches on endpoint identity.ANTHROPIC_AUTH_TOKENrung, ahead of the credentials file.Scope
Only airframe's own direct HTTP calls. The
claudeCLI subprocess still resolves auth from the inherited environment — that's what makes pointing the adapter at a compatible endpoint work at all, and it's the CLI's decision to make.Tests
18 new (
1024 → 1042),make cigreen. Covers the parametrized hostname matrix (includingevilanthropic.comandapi.anthropic.com.evil.testas negatives), both withholding paths, the refusal,count_tokensparity, and that an explicitapi_key=still works against a third-party URL. An autouse fixture clears ambientANTHROPIC_BASE_URL/ANTHROPIC_AUTH_TOKENso the suite behaves the same on a developer box as in CI.🤖 Generated with Claude Code
https://claude.ai/code/session_01UBTr6Q6kTGUMQiwwBHcdMj