claude-code direct subscription connection#
The claude-code provider connects directly to api.anthropic.com with the
anthropic SDK, using a subscription OAuth token — the same shape as
openai-codex, which reads ~/.codex/auth.json directly and connects directly
to chatgpt.com/backend-api. There is no local proxy daemon in the path.
Two constraints shape the design:
- The provider is named
claude-codein the WebUI and CLI. Only the underlying Runtime is Anthropic-direct; the user-facing name does not change. - Credentials live in OpenProgram's own system (AuthStore plus the
~/.claude/.credentials.jsonfile). The macOS Keychain is not touched.
Why a proxy is not required#
anthropic.py:245-261 supports subscription OAuth directly: when the token is
sk-ant-oat…, the request goes to the Messages API with auth_token=<token>,
anthropic-beta: claude-code-20250219,oauth-2025-04-20,…, and
user-agent: claude-cli/<ver>. This is the same approach codex uses against
chatgpt.com.
The two arguments that once favoured a proxy do not hold. A Max account exposing
no api.anthropic.com key is true but irrelevant — the subscription uses an
OAuth token, not an api-key, and the direct connection needs only a Bearer
token plus the beta header. And image blocks arriving as [object Object] was a
bug in one particular proxy; the official anthropic SDK supports image blocks
natively against the Messages API, so multimodal content survives.
Credential forms#
| Credential form | Source | kind | refresh |
|---|---|---|---|
| Observe the Claude CLI | ~/.claude/.credentials.json |
cli_delegated |
The Claude CLI refreshes itself; OpenProgram observes and re-reads |
| Self-held api-key | openprogram auth login anthropic --api-key |
api_key |
Doesn't expire |
The cli_delegated mode mirrors codex exactly. The codex CLI maintains
~/.codex/auth.json and OpenProgram re-reads the latest access_token on each
use; likewise the Claude CLI maintains ~/.claude/.credentials.json (a plain
file on Linux and Windows, read directly) and OpenProgram re-reads
claudeAiOauth.accessToken on each use. Refresh is the external CLI's
responsibility, which is what makes the mode cheap.
Mechanics#
Token extraction. auth/resolver.py:_extract_token re-reads store_path
for a CliDelegatedPayload and pulls the access_token from access_key_path.
This is general to the credential kind, so codex's cli_delegated uses the same
path.
Unified resolution in the anthropic provider. stream_simple in
providers/anthropic/anthropic.py and registry.py's anthropic
create_runtime path both resolve the token through
resolve_api_key_sync(provider), which covers OAuth, cli_delegated, and
manager-driven refresh.
Registry. providers/registry.py maps "claude-code" to the direct
Runtime. That Runtime is lightweight: its models go through the
anthropic:<id> namespace, reusing the anthropic provider's wire, and its token
resolves from the anthropic pool. Model alias normalisation (opus / sonnet /
haiku) carries over.
Expiry. When a cli_delegated credential expires, CredentialProvider raises
AuthReadOnlyError — the credential is read-only and cannot refresh itself —
and the message directs the user to claude login. The direct path reuses this
rather than adding its own expiry handling.
The api="claude-code-cli" wire label is declared in
_claude_code_registry.py and has no consumer; requests always go through
Runtime to the anthropic:<id> Messages wire, so no wire implementation is
involved.
Subscription login#
The direct connection covers using a token; login covers how the token arrives. claude-code uses the same PKCE framework as codex.
- OAuth parameters live in
auth_adapter.py:OAUTH_CLIENT_ID=9d1c250a-e61b-44d9-88ed-5944d1962f5e, authorize =claude.ai/oauth/authorize, token =console.anthropic.com/v1/oauth/token, redirect =console.anthropic.com/oauth/code/callback.build_pkce_config()uses manual-paste mode, because Anthropic is a hosted redirect that displayscode#staterather than a loopback callback, plus token JSON. - The shared PKCE framework carries three switches for this —
manual_paste_only,redirect_uri_override,token_use_json— along with_credential_from_tokensextraction and exchange with state, all inpkce_oauth.py. They generalize the framework rather than special-casing Anthropic inside it. - Refresh is
_anthropic_refresh(the refresh_token is swapped for a new one, JSON), registered on ProviderAuthConfig. Credentials with no refresh_token, such as setup-token, no-op automatically. - setup-token goes through
import_setup_token, which stores an oauth kind with an empty refresh_token and roughly a year's expiry. - Login methods for anthropic and claude-code are
pkce_oauth(default) andsetup_tokenonly. Importing from~/.claudeand pasting an api key are not offered. - The driver (
login_driver) has an anthropic pkce branch plus setup_token dispatch;_credential_provider_idmaps claude-code to anthropic so the credential lands in the anthropic pool. - Multiple accounts are one profile each, reusing unified account management and 429 rotation.
Account management in the WebUI#
claude-code's accounts go through the general account routes, not
provider-specific ones. webui/routes/accounts.py maps claude-code to the
anthropic pool via _pool_id, so every general route stores and fetches by
pool, and _api_key_env returns "" for claude-code, which forces
add_mode=login and hides the key-paste field. setup_hints.py describes the
provider as a direct Anthropic connection over subscription OAuth and explains
the two login methods.
The frontend needs no claude-code branch: account-manager.tsx and
provider-login.tsx are data-driven, so add_mode=login renders the two login
buttons on its own.
Implementation status#
The direct connection, subscription login, and WebUI account management described above are all in place:
auth/resolver.py:_extract_tokenplus_read_delegated_tokenre-readstore_pathforcli_delegated.providers/anthropic/anthropic.py:stream_simpleandregistry.py:_http_api_key_for(theanthropiccreate_runtime path) resolve throughresolve_api_key_sync.providers/anthropic/_claude_code_direct_runtime.pyholds the direct ClaudeCodeRuntime.providers/registry.pypointsclaude-codeat it.tests/unit/test_claude_code_direct_oauth.pycovers the path;test_runtime_key_ladder.pymock points target the unified resolution.
_max_proxy_runtime.py, _claude_max_proxy_registry.py, and _meridian_cli.py
remain on disk but are no longer reached by any route or referenced by the
registry. They are removable once the WebUI's remaining "Add Claude account"
references are confirmed gone.