From 453a1452bbf8842faad2a76f4352a579354d940b Mon Sep 17 00:00:00 2001 From: Menny Even Danan Date: Wed, 4 Mar 2026 04:01:28 -0500 Subject: [PATCH] Add support for ANTHROPIC_API_KEY environment variable authentication detection (#346) * Add support for ANTHROPIC_API_KEY environment variable authentication detection This commit enhances Claude authentication detection to support both the ANTHROPIC_API_KEY environment variable and the OAuth credentials file, matching the authentication priority order used by the Claude Agent SDK. - Updated checkClaudeCredentials() function in server/routes/cli-auth.js to check ANTHROPIC_API_KEY environment variable first, then fall back to ~/.claude/.credentials.json OAuth tokens - Modified /api/cli-auth/claude/status endpoint to return authentication method indicator ('api_key' or 'credentials_file') - Added comprehensive JSDoc documentation with priority order explanation and official Claude documentation citations 1. ANTHROPIC_API_KEY environment variable (highest priority) 2. ~/.claude/.credentials.json OAuth tokens (fallback) This priority order matches the Claude Agent SDK's authentication behavior, ensuring consistency between how we detect authentication and how the SDK actually authenticates. The /api/cli-auth/claude/status endpoint now returns: - method: 'api_key' when using ANTHROPIC_API_KEY environment variable - method: 'credentials_file' when using OAuth credentials file - method: null when not authenticated This is backward compatible as existing code checking the 'authenticated' field will continue to work. - https://support.claude.com/en/articles/12304248-managing-api-key-environment-variables-in-claude-code Claude Agent SDK prioritizes environment variables over subscriptions - https://platform.claude.com/docs/en/agent-sdk/overview Official Claude Agent SDK authentication documentation When ANTHROPIC_API_KEY is set, API calls are charged via pay-as-you-go rates instead of subscription rates, even if the user is logged in with a claude.ai subscription. * UI: hide Claude login when API key auth is used An API key overrides anything else with Claude SDK (and claude-code). There is no need to show this button in API key cases. --- server/routes/cli-auth.js | 27 +++++++++++-- .../settings/hooks/useSettingsController.ts | 2 + src/components/settings/types/types.ts | 1 + .../sections/content/AccountContent.tsx | 40 ++++++++++--------- 4 files changed, 47 insertions(+), 23 deletions(-) diff --git a/server/routes/cli-auth.js b/server/routes/cli-auth.js index 9b3721bf..cb7275dc 100644 --- a/server/routes/cli-auth.js +++ b/server/routes/cli-auth.js @@ -14,13 +14,14 @@ router.get('/claude/status', async (req, res) => { return res.json({ authenticated: true, email: credentialsResult.email || 'Authenticated', - method: 'credentials_file' + method: credentialsResult.method // 'api_key' or 'credentials_file' }); } return res.json({ authenticated: false, email: null, + method: null, error: credentialsResult.error || 'Not authenticated' }); @@ -29,6 +30,7 @@ router.get('/claude/status', async (req, res) => { res.status(500).json({ authenticated: false, email: null, + method: null, error: error.message }); } @@ -115,6 +117,20 @@ router.get('/gemini/status', async (req, res) => { * - method: 'api_key' for env var, 'credentials_file' for OAuth tokens */ async function checkClaudeCredentials() { + // Priority 1: Check for ANTHROPIC_API_KEY environment variable + // The SDK checks this first and uses it if present, even if OAuth tokens exist. + // When set, API calls are charged via pay-as-you-go rates instead of subscription. + if (process.env.ANTHROPIC_API_KEY && process.env.ANTHROPIC_API_KEY.trim()) { + return { + authenticated: true, + email: 'API Key Auth', + method: 'api_key' + }; + } + + // Priority 2: Check ~/.claude/.credentials.json for OAuth tokens + // This is the standard authentication method used by Claude CLI after running + // 'claude /login' or 'claude setup-token' commands. try { const credPath = path.join(os.homedir(), '.claude', '.credentials.json'); const content = await fs.readFile(credPath, 'utf8'); @@ -127,19 +143,22 @@ async function checkClaudeCredentials() { if (!isExpired) { return { authenticated: true, - email: creds.email || creds.user || null + email: creds.email || creds.user || null, + method: 'credentials_file' }; } } return { authenticated: false, - email: null + email: null, + method: null }; } catch (error) { return { authenticated: false, - email: null + email: null, + method: null }; } } diff --git a/src/components/settings/hooks/useSettingsController.ts b/src/components/settings/hooks/useSettingsController.ts index ad0c3068..b0f8ab86 100644 --- a/src/components/settings/hooks/useSettingsController.ts +++ b/src/components/settings/hooks/useSettingsController.ts @@ -41,6 +41,7 @@ type StatusApiResponse = { authenticated?: boolean; email?: string | null; error?: string | null; + method?: string; }; type JsonResult = { @@ -267,6 +268,7 @@ export function useSettingsController({ isOpen, initialTab, projects, onClose }: email: data.email || null, loading: false, error: data.error || null, + method: data.method, }); } catch (error) { console.error(`Error checking ${provider} auth status:`, error); diff --git a/src/components/settings/types/types.ts b/src/components/settings/types/types.ts index 60cd3d9a..7466ef4a 100644 --- a/src/components/settings/types/types.ts +++ b/src/components/settings/types/types.ts @@ -23,6 +23,7 @@ export type AuthStatus = { email: string | null; loading: boolean; error: string | null; + method?: string; }; export type KeyValueMap = Record; diff --git a/src/components/settings/view/tabs/agents-settings/sections/content/AccountContent.tsx b/src/components/settings/view/tabs/agents-settings/sections/content/AccountContent.tsx index 858e7f67..535e3729 100644 --- a/src/components/settings/view/tabs/agents-settings/sections/content/AccountContent.tsx +++ b/src/components/settings/view/tabs/agents-settings/sections/content/AccountContent.tsx @@ -107,28 +107,30 @@ export default function AccountContent({ agent, authStatus, onLogin }: AccountCo -
-
-
-
- {authStatus.authenticated ? t('agents.login.reAuthenticate') : t('agents.login.title')} -
-
- {authStatus.authenticated - ? t('agents.login.reAuthDescription') - : t('agents.login.description', { agent: config.name })} + {authStatus.method !== 'api_key' && ( +
+
+
+
+ {authStatus.authenticated ? t('agents.login.reAuthenticate') : t('agents.login.title')} +
+
+ {authStatus.authenticated + ? t('agents.login.reAuthDescription') + : t('agents.login.description', { agent: config.name })} +
+
-
-
+ )} {authStatus.error && (