From cce72928775e56bc191944c723fb82fee0bf4710 Mon Sep 17 00:00:00 2001 From: "Kuranasaki@M3Max" Date: Wed, 8 Jul 2026 16:55:55 +0700 Subject: [PATCH 1/6] Fix broken auto import --- packages/ui/src/pages/home/App.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/pages/home/App.tsx b/packages/ui/src/pages/home/App.tsx index 5bd3b5e..c6b61da 100644 --- a/packages/ui/src/pages/home/App.tsx +++ b/packages/ui/src/pages/home/App.tsx @@ -60,12 +60,17 @@ const localAgentProviderApiKey = "ccr-local-agent-login"; function materializeProviderPluginTemplates( templates: unknown[], providerName: string, - protocol: GatewayProviderConfig["type"] + protocol: GatewayProviderConfig["type"], + providerId: string ): unknown[] { if (templates.length === 0) { return []; } - const internalName = protocol ? `${providerName}::${protocol}` : providerName; + // The core gateway matches provider plugins against the provider's runtime + // identifier (provider.id, or its slug), not the human-readable display name + // — the internal name here must mirror providerCapabilityInternalName() in + // gateway/service.ts or the plugin's auth-header override silently never applies. + const internalName = protocol ? `${providerId}::${protocol}` : providerId; const replacements: Record = { [providerInternalNamePlaceholder]: internalName, [providerNamePlaceholder]: providerName, @@ -1431,6 +1436,8 @@ function App() { return false; } + const existingProvider = providerEditIndex !== undefined ? draftConfig.Providers[providerEditIndex] : undefined; + const providerId = existingProvider?.id ?? providerNameSlug(providerName); const provider: GatewayProviderConfig = { api_base_url: normalizeProviderBaseUrl(baseUrl, protocol), api_key: providerDraft.apiKey.trim(), @@ -1438,13 +1445,14 @@ function App() { account: accountConfig, credentials: credentials.length > 0 ? credentials : undefined, icon: providerDraft.icon.trim() || undefined, + id: providerId, modelDescriptions, modelDisplayNames, models, name: providerName, type: protocol }; - const importedProviderPlugins = materializeProviderPluginTemplates(providerDraft.providerPlugins, providerName, protocol); + const importedProviderPlugins = materializeProviderPluginTemplates(providerDraft.providerPlugins, providerName, protocol, providerId); const wasImport = providerImportOpen; const next = buildConfigUpdate((config) => { From d8154e5357f2773eae267c8eb755fbf741cf4483 Mon Sep 17 00:00:00 2001 From: "Kuranasaki@M3Max" Date: Wed, 8 Jul 2026 17:21:35 +0700 Subject: [PATCH 2/6] Read claude credential from KeyChain --- issue.md | 109 ++++++++++++++++++ issue2.md | 39 +++++++ .../src/agents/local-providers/claude-code.ts | 38 ++++++ 3 files changed, 186 insertions(+) create mode 100644 issue.md create mode 100644 issue2.md diff --git a/issue.md b/issue.md new file mode 100644 index 0000000..f4b970b --- /dev/null +++ b/issue.md @@ -0,0 +1,109 @@ +# Local-agent OAuth provider plugin auth override never applies + +## Summary + +Providers imported from a local CLI login (Claude Code OAuth, Codex OAuth) send +requests upstream with the wrong auth header. Instead of +`Authorization: Bearer ` (plus `anthropic-beta: oauth-2025-04-20`), +the request goes out with `x-api-key: ccr-local-agent-login` (the internal +placeholder credential) or, after a partial fix, `x-api-key: ` +— still the wrong header, since Anthropic's OAuth flow requires `Authorization`, +not `x-api-key`. + +## Root cause + +Importing a local-agent provider (`packages/core/src/agents/local-providers/claude-code.ts`, +`codex.ts`) creates two things: + +1. A `GatewayProviderConfig` entry in `config.Providers`, with `api_key` set to + the sentinel placeholder `ccr-local-agent-login` + (`packages/core/src/agents/local-providers/shared.ts:26`). +2. One or more `providerPlugins` entries (`bearerAuthPlugin()` / + `apiKeyAuthPlugin()` in `shared.ts:78-114`) carrying the real captured OAuth + token in `auth.headers.authorization`, plus `removeHeaders: ["x-api-key"]`. + +The plugin is supposed to be matched to its provider by the internal gateway +process (`@the-next-ai/ai-gateway`, config written by `writeCoreGatewayConfig()` +in `packages/core/src/gateway/service.ts:1151`) via an **exact string match** +between the plugin's `providerName` field and the provider's *runtime* name. + +The runtime name is computed by `providerRuntimeId()` +(`gateway/service.ts:6671`): + +```ts +function providerRuntimeId(provider) { + const explicit = sanitizeProviderHeaderId(provider.id); + if (explicit) return explicit; + // ...falls back to `provider--` +} +``` + +`provider.id` was **never set** during import +(`packages/ui/src/pages/home/App.tsx`, provider-save handler around line 1434 +— no `id` field on the constructed `GatewayProviderConfig`). So the backend +always fell into the hash branch, producing an opaque name like +`provider-claude-code-api-884b99c439::anthropic_messages`. + +Meanwhile, the plugin's `providerName` was set (in +`materializeProviderPluginTemplates()`, `App.tsx:60-75`) to the **human-readable +label**: `"Claude Code API::anthropic_messages"`. + +These two strings never match. The gateway's plugin-resolution step +(`resolve(n, t)` in the vendor bundle) silently no-ops — no error, no log — +and the provider's raw `api_key` (the sentinel, or later the swapped-in real +token) goes out using the protocol's default header convention. For Anthropic +(`type: "anthropic"`), that default is always `x-api-key`, never `Authorization`, +regardless of `extraHeaders`. + +## Fix applied + +Two changes, both to make the plugin's `providerName` deterministically equal +to whatever runtime name the provider will actually get — rather than trying +to intercept/patch headers after the fact. + +**1. `packages/ui/src/pages/home/App.tsx` (generator/import-time fix, kept):** + +- Provider save now sets an explicit `id` on the saved `GatewayProviderConfig`: + a slug of the provider name (`providerNameSlug(providerName)`), reusing the + existing edit's `id` if editing rather than importing fresh. +- `materializeProviderPluginTemplates()` now builds the plugin's internal name + from that same `id` (`${providerId}::${protocol}`) instead of the raw human + name, so it exactly matches `providerCapabilityInternalName()` on the + backend (which now takes the explicit `id` path in `providerRuntimeId()`, + skipping the hash entirely — fully deterministic). + +Verified: after wiping `~/.claude-code-router` and re-importing, generated +`gateway.config.json` shows the provider and its paired plugin sharing the +same computed name (`claude-code-api::anthropic_messages`), where previously +they diverged (`Claude Code API` vs `provider-claude-code-api-::anthropic_messages`). + +**2. `packages/core/src/gateway/service.ts` (write-time defense-in-depth, +currently stashed / not applied):** + +- `toCoreGatewayProvider()` swaps the sentinel `api_key` for the real resolved + credential (via `localAgentProviderAccountCredential()`, exported from + `packages/core/src/providers/account-service.ts`) before handing the + provider to the gateway. +- `writeCoreGatewayConfig()` also expands each `providerPlugins` entry's + `providerName` into every actual runtime alias (bare name, `name::protocol`) + via a new `withProviderPluginRuntimeNames()` alias-map, so plugins still + match even if a provider was imported before the fix in (1). +- Held back at the user's request in favor of the cleaner root-cause fix in + (1); re-apply from `git stash@{0}` ("Fixed claude oat") if existing + pre-fix-1 imports need to keep working without re-import. + +## Known follow-up: newer macOS Claude Code CLI stores credentials in Keychain + +`~/.claude/.credentials.json` is Claude Code CLI's older on-disk credential +store. Recent macOS builds instead store the OAuth token in the macOS +Keychain under generic-password service name `"Claude Code-credentials"`. +CCR's importer (`claude-code.ts`) only reads the JSON file today — if it's +absent, import silently finds nothing to import. + +Fix not yet implemented: in the importer, fall back to +`security find-generic-password -s "Claude Code-credentials" -w` (triggers a +normal macOS keychain-access permission prompt) when the credentials file is +missing, parse stdout as JSON, and feed it through the same +`findOauthTokenSet()` path already used for the file-based case. Needs a +try/catch since `security` exits non-zero if the user denies the prompt or no +such keychain item exists. diff --git a/issue2.md b/issue2.md new file mode 100644 index 0000000..3ba7426 --- /dev/null +++ b/issue2.md @@ -0,0 +1,39 @@ +# Claude Code CLI credentials moved to macOS Keychain — local-agent import fails + +## Problem + +Newer macOS builds of the Claude Code CLI store OAuth credentials in the macOS +Keychain (service name `"Claude Code-credentials"`) instead of +`~/.claude/.credentials.json`. CCR's local-agent provider importer only reads +the file path, so on these installs it finds no credentials and the +"Claude Code API" local login provider cannot be detected/imported. + +## Where it's read today + +`packages/core/src/agents/local-providers/claude-code.ts` uses +`readJsonRecord()` (from `packages/core/src/agents/local-providers/shared.ts`) +to read `~/.claude/.credentials.json` directly off disk. There is no +Keychain fallback anywhere in `packages/core/src/agents/local-providers/`. + +## Fix direction + +Add a Keychain fallback when the credentials file is missing/empty: + +```bash +security find-generic-password -s "Claude Code-credentials" -w +``` + +- Triggers a standard macOS permission prompt (Allow / Always Allow) on + first access — no bypass, same consent flow any app goes through. +- Exits non-zero if the item doesn't exist or the user denies access — + must be wrapped in try/catch, falling back to "no local credentials + found" (existing `missingCandidate()` path) rather than throwing. +- Output is JSON on stdout, same shape expected by + `findOauthTokenSet()` in `shared.ts` — parse and feed through the same + path used for the file-based case. +- macOS-only path (`process.platform === "darwin"`); other platforms keep + using the file-based read only. + +## Not yet implemented + +This is a plan, not a diff — nothing has been changed for this issue yet. diff --git a/packages/core/src/agents/local-providers/claude-code.ts b/packages/core/src/agents/local-providers/claude-code.ts index 37c05ff..00e2032 100644 --- a/packages/core/src/agents/local-providers/claude-code.ts +++ b/packages/core/src/agents/local-providers/claude-code.ts @@ -1,3 +1,4 @@ +import { execFileSync } from "node:child_process"; import os from "node:os"; import path from "node:path"; import type { @@ -9,6 +10,7 @@ import type { import { bearerAuthPlugin, findOauthTokenSet, + isRecord, missingCandidate, providerInternalNamePlaceholder, providerPayload, @@ -18,6 +20,8 @@ import { type OAuthTokenSet } from "@ccr/core/agents/local-providers/shared"; +const claudeCodeKeychainService = "Claude Code-credentials"; + const claudeDefaultModels = ["claude-sonnet-4-20250514"]; const percentLimitMapping = (id: string, label: string, path: string, window: string) => ({ @@ -148,6 +152,19 @@ function readClaudeCodeOauth(): OAuthTokenSet | undefined { sourceFile }; } + + const keychainRecord = readClaudeCodeKeychainRecord(); + if (keychainRecord) { + const credential = findOauthTokenSet(keychainRecord); + if (credential) { + return { + accessToken: credential.accessToken, + refreshToken: credential.refreshToken, + sourceFile: `keychain:${claudeCodeKeychainService}` + }; + } + } + return undefined; } @@ -158,3 +175,24 @@ function claudeCredentialFiles(): string[] { path.join(os.homedir(), ".config", "claude", "credentials.json") ]); } + +// Newer macOS builds of the Claude Code CLI store credentials in the +// Keychain instead of ~/.claude/.credentials.json. Reading it triggers the +// standard macOS keychain access prompt (Allow / Always Allow); the user +// declining or the item not existing both surface as a non-zero exit here. +function readClaudeCodeKeychainRecord(): Record | undefined { + if (process.platform !== "darwin") { + return undefined; + } + try { + const output = execFileSync( + "security", + ["find-generic-password", "-s", claudeCodeKeychainService, "-w"], + { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] } + ); + const parsed = JSON.parse(output.trim()) as unknown; + return isRecord(parsed) ? parsed : undefined; + } catch { + return undefined; + } +} From a65b8bb6f960bfe22f7ac92df99945e7ef6597d6 Mon Sep 17 00:00:00 2001 From: "Kuranasaki@M3Max" Date: Wed, 8 Jul 2026 17:23:13 +0700 Subject: [PATCH 3/6] Read claude credential from KeyChain --- .../src/agents/local-providers/claude-code.ts | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/packages/core/src/agents/local-providers/claude-code.ts b/packages/core/src/agents/local-providers/claude-code.ts index 37c05ff..00e2032 100644 --- a/packages/core/src/agents/local-providers/claude-code.ts +++ b/packages/core/src/agents/local-providers/claude-code.ts @@ -1,3 +1,4 @@ +import { execFileSync } from "node:child_process"; import os from "node:os"; import path from "node:path"; import type { @@ -9,6 +10,7 @@ import type { import { bearerAuthPlugin, findOauthTokenSet, + isRecord, missingCandidate, providerInternalNamePlaceholder, providerPayload, @@ -18,6 +20,8 @@ import { type OAuthTokenSet } from "@ccr/core/agents/local-providers/shared"; +const claudeCodeKeychainService = "Claude Code-credentials"; + const claudeDefaultModels = ["claude-sonnet-4-20250514"]; const percentLimitMapping = (id: string, label: string, path: string, window: string) => ({ @@ -148,6 +152,19 @@ function readClaudeCodeOauth(): OAuthTokenSet | undefined { sourceFile }; } + + const keychainRecord = readClaudeCodeKeychainRecord(); + if (keychainRecord) { + const credential = findOauthTokenSet(keychainRecord); + if (credential) { + return { + accessToken: credential.accessToken, + refreshToken: credential.refreshToken, + sourceFile: `keychain:${claudeCodeKeychainService}` + }; + } + } + return undefined; } @@ -158,3 +175,24 @@ function claudeCredentialFiles(): string[] { path.join(os.homedir(), ".config", "claude", "credentials.json") ]); } + +// Newer macOS builds of the Claude Code CLI store credentials in the +// Keychain instead of ~/.claude/.credentials.json. Reading it triggers the +// standard macOS keychain access prompt (Allow / Always Allow); the user +// declining or the item not existing both surface as a non-zero exit here. +function readClaudeCodeKeychainRecord(): Record | undefined { + if (process.platform !== "darwin") { + return undefined; + } + try { + const output = execFileSync( + "security", + ["find-generic-password", "-s", claudeCodeKeychainService, "-w"], + { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] } + ); + const parsed = JSON.parse(output.trim()) as unknown; + return isRecord(parsed) ? parsed : undefined; + } catch { + return undefined; + } +} From 421cbd05b2354c232c2fbb45c238632f304229aa Mon Sep 17 00:00:00 2001 From: "Kuranasaki@M3Max" Date: Wed, 8 Jul 2026 17:26:51 +0700 Subject: [PATCH 4/6] Update default claude model to claude-sonnet-4-5-20250929 --- packages/core/src/agents/local-providers/claude-code.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/agents/local-providers/claude-code.ts b/packages/core/src/agents/local-providers/claude-code.ts index 00e2032..f712fcd 100644 --- a/packages/core/src/agents/local-providers/claude-code.ts +++ b/packages/core/src/agents/local-providers/claude-code.ts @@ -22,7 +22,7 @@ import { const claudeCodeKeychainService = "Claude Code-credentials"; -const claudeDefaultModels = ["claude-sonnet-4-20250514"]; +const claudeDefaultModels = ["claude-sonnet-4-5-20250929"]; const percentLimitMapping = (id: string, label: string, path: string, window: string) => ({ id, From f56896867381f9947fbec1d04fe2642d22a65d88 Mon Sep 17 00:00:00 2001 From: "Kuranasaki@M3Max" Date: Wed, 8 Jul 2026 17:29:59 +0700 Subject: [PATCH 5/6] Revert model change --- packages/core/src/agents/local-providers/claude-code.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/agents/local-providers/claude-code.ts b/packages/core/src/agents/local-providers/claude-code.ts index f712fcd..00e2032 100644 --- a/packages/core/src/agents/local-providers/claude-code.ts +++ b/packages/core/src/agents/local-providers/claude-code.ts @@ -22,7 +22,7 @@ import { const claudeCodeKeychainService = "Claude Code-credentials"; -const claudeDefaultModels = ["claude-sonnet-4-5-20250929"]; +const claudeDefaultModels = ["claude-sonnet-4-20250514"]; const percentLimitMapping = (id: string, label: string, path: string, window: string) => ({ id, From 70f85864a62ea4018c598520d56313ecf42b67bf Mon Sep 17 00:00:00 2001 From: "Kuranasaki@M3Max" Date: Wed, 8 Jul 2026 17:46:24 +0700 Subject: [PATCH 6/6] Revert "Read claude credential from KeyChain" This reverts commit d8154e5357f2773eae267c8eb755fbf741cf4483. --- issue.md | 109 ------------------ issue2.md | 39 ------- .../src/agents/local-providers/claude-code.ts | 38 ------ 3 files changed, 186 deletions(-) delete mode 100644 issue.md delete mode 100644 issue2.md diff --git a/issue.md b/issue.md deleted file mode 100644 index f4b970b..0000000 --- a/issue.md +++ /dev/null @@ -1,109 +0,0 @@ -# Local-agent OAuth provider plugin auth override never applies - -## Summary - -Providers imported from a local CLI login (Claude Code OAuth, Codex OAuth) send -requests upstream with the wrong auth header. Instead of -`Authorization: Bearer ` (plus `anthropic-beta: oauth-2025-04-20`), -the request goes out with `x-api-key: ccr-local-agent-login` (the internal -placeholder credential) or, after a partial fix, `x-api-key: ` -— still the wrong header, since Anthropic's OAuth flow requires `Authorization`, -not `x-api-key`. - -## Root cause - -Importing a local-agent provider (`packages/core/src/agents/local-providers/claude-code.ts`, -`codex.ts`) creates two things: - -1. A `GatewayProviderConfig` entry in `config.Providers`, with `api_key` set to - the sentinel placeholder `ccr-local-agent-login` - (`packages/core/src/agents/local-providers/shared.ts:26`). -2. One or more `providerPlugins` entries (`bearerAuthPlugin()` / - `apiKeyAuthPlugin()` in `shared.ts:78-114`) carrying the real captured OAuth - token in `auth.headers.authorization`, plus `removeHeaders: ["x-api-key"]`. - -The plugin is supposed to be matched to its provider by the internal gateway -process (`@the-next-ai/ai-gateway`, config written by `writeCoreGatewayConfig()` -in `packages/core/src/gateway/service.ts:1151`) via an **exact string match** -between the plugin's `providerName` field and the provider's *runtime* name. - -The runtime name is computed by `providerRuntimeId()` -(`gateway/service.ts:6671`): - -```ts -function providerRuntimeId(provider) { - const explicit = sanitizeProviderHeaderId(provider.id); - if (explicit) return explicit; - // ...falls back to `provider--` -} -``` - -`provider.id` was **never set** during import -(`packages/ui/src/pages/home/App.tsx`, provider-save handler around line 1434 -— no `id` field on the constructed `GatewayProviderConfig`). So the backend -always fell into the hash branch, producing an opaque name like -`provider-claude-code-api-884b99c439::anthropic_messages`. - -Meanwhile, the plugin's `providerName` was set (in -`materializeProviderPluginTemplates()`, `App.tsx:60-75`) to the **human-readable -label**: `"Claude Code API::anthropic_messages"`. - -These two strings never match. The gateway's plugin-resolution step -(`resolve(n, t)` in the vendor bundle) silently no-ops — no error, no log — -and the provider's raw `api_key` (the sentinel, or later the swapped-in real -token) goes out using the protocol's default header convention. For Anthropic -(`type: "anthropic"`), that default is always `x-api-key`, never `Authorization`, -regardless of `extraHeaders`. - -## Fix applied - -Two changes, both to make the plugin's `providerName` deterministically equal -to whatever runtime name the provider will actually get — rather than trying -to intercept/patch headers after the fact. - -**1. `packages/ui/src/pages/home/App.tsx` (generator/import-time fix, kept):** - -- Provider save now sets an explicit `id` on the saved `GatewayProviderConfig`: - a slug of the provider name (`providerNameSlug(providerName)`), reusing the - existing edit's `id` if editing rather than importing fresh. -- `materializeProviderPluginTemplates()` now builds the plugin's internal name - from that same `id` (`${providerId}::${protocol}`) instead of the raw human - name, so it exactly matches `providerCapabilityInternalName()` on the - backend (which now takes the explicit `id` path in `providerRuntimeId()`, - skipping the hash entirely — fully deterministic). - -Verified: after wiping `~/.claude-code-router` and re-importing, generated -`gateway.config.json` shows the provider and its paired plugin sharing the -same computed name (`claude-code-api::anthropic_messages`), where previously -they diverged (`Claude Code API` vs `provider-claude-code-api-::anthropic_messages`). - -**2. `packages/core/src/gateway/service.ts` (write-time defense-in-depth, -currently stashed / not applied):** - -- `toCoreGatewayProvider()` swaps the sentinel `api_key` for the real resolved - credential (via `localAgentProviderAccountCredential()`, exported from - `packages/core/src/providers/account-service.ts`) before handing the - provider to the gateway. -- `writeCoreGatewayConfig()` also expands each `providerPlugins` entry's - `providerName` into every actual runtime alias (bare name, `name::protocol`) - via a new `withProviderPluginRuntimeNames()` alias-map, so plugins still - match even if a provider was imported before the fix in (1). -- Held back at the user's request in favor of the cleaner root-cause fix in - (1); re-apply from `git stash@{0}` ("Fixed claude oat") if existing - pre-fix-1 imports need to keep working without re-import. - -## Known follow-up: newer macOS Claude Code CLI stores credentials in Keychain - -`~/.claude/.credentials.json` is Claude Code CLI's older on-disk credential -store. Recent macOS builds instead store the OAuth token in the macOS -Keychain under generic-password service name `"Claude Code-credentials"`. -CCR's importer (`claude-code.ts`) only reads the JSON file today — if it's -absent, import silently finds nothing to import. - -Fix not yet implemented: in the importer, fall back to -`security find-generic-password -s "Claude Code-credentials" -w` (triggers a -normal macOS keychain-access permission prompt) when the credentials file is -missing, parse stdout as JSON, and feed it through the same -`findOauthTokenSet()` path already used for the file-based case. Needs a -try/catch since `security` exits non-zero if the user denies the prompt or no -such keychain item exists. diff --git a/issue2.md b/issue2.md deleted file mode 100644 index 3ba7426..0000000 --- a/issue2.md +++ /dev/null @@ -1,39 +0,0 @@ -# Claude Code CLI credentials moved to macOS Keychain — local-agent import fails - -## Problem - -Newer macOS builds of the Claude Code CLI store OAuth credentials in the macOS -Keychain (service name `"Claude Code-credentials"`) instead of -`~/.claude/.credentials.json`. CCR's local-agent provider importer only reads -the file path, so on these installs it finds no credentials and the -"Claude Code API" local login provider cannot be detected/imported. - -## Where it's read today - -`packages/core/src/agents/local-providers/claude-code.ts` uses -`readJsonRecord()` (from `packages/core/src/agents/local-providers/shared.ts`) -to read `~/.claude/.credentials.json` directly off disk. There is no -Keychain fallback anywhere in `packages/core/src/agents/local-providers/`. - -## Fix direction - -Add a Keychain fallback when the credentials file is missing/empty: - -```bash -security find-generic-password -s "Claude Code-credentials" -w -``` - -- Triggers a standard macOS permission prompt (Allow / Always Allow) on - first access — no bypass, same consent flow any app goes through. -- Exits non-zero if the item doesn't exist or the user denies access — - must be wrapped in try/catch, falling back to "no local credentials - found" (existing `missingCandidate()` path) rather than throwing. -- Output is JSON on stdout, same shape expected by - `findOauthTokenSet()` in `shared.ts` — parse and feed through the same - path used for the file-based case. -- macOS-only path (`process.platform === "darwin"`); other platforms keep - using the file-based read only. - -## Not yet implemented - -This is a plan, not a diff — nothing has been changed for this issue yet. diff --git a/packages/core/src/agents/local-providers/claude-code.ts b/packages/core/src/agents/local-providers/claude-code.ts index 00e2032..37c05ff 100644 --- a/packages/core/src/agents/local-providers/claude-code.ts +++ b/packages/core/src/agents/local-providers/claude-code.ts @@ -1,4 +1,3 @@ -import { execFileSync } from "node:child_process"; import os from "node:os"; import path from "node:path"; import type { @@ -10,7 +9,6 @@ import type { import { bearerAuthPlugin, findOauthTokenSet, - isRecord, missingCandidate, providerInternalNamePlaceholder, providerPayload, @@ -20,8 +18,6 @@ import { type OAuthTokenSet } from "@ccr/core/agents/local-providers/shared"; -const claudeCodeKeychainService = "Claude Code-credentials"; - const claudeDefaultModels = ["claude-sonnet-4-20250514"]; const percentLimitMapping = (id: string, label: string, path: string, window: string) => ({ @@ -152,19 +148,6 @@ function readClaudeCodeOauth(): OAuthTokenSet | undefined { sourceFile }; } - - const keychainRecord = readClaudeCodeKeychainRecord(); - if (keychainRecord) { - const credential = findOauthTokenSet(keychainRecord); - if (credential) { - return { - accessToken: credential.accessToken, - refreshToken: credential.refreshToken, - sourceFile: `keychain:${claudeCodeKeychainService}` - }; - } - } - return undefined; } @@ -175,24 +158,3 @@ function claudeCredentialFiles(): string[] { path.join(os.homedir(), ".config", "claude", "credentials.json") ]); } - -// Newer macOS builds of the Claude Code CLI store credentials in the -// Keychain instead of ~/.claude/.credentials.json. Reading it triggers the -// standard macOS keychain access prompt (Allow / Always Allow); the user -// declining or the item not existing both surface as a non-zero exit here. -function readClaudeCodeKeychainRecord(): Record | undefined { - if (process.platform !== "darwin") { - return undefined; - } - try { - const output = execFileSync( - "security", - ["find-generic-password", "-s", claudeCodeKeychainService, "-w"], - { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] } - ); - const parsed = JSON.parse(output.trim()) as unknown; - return isRecord(parsed) ? parsed : undefined; - } catch { - return undefined; - } -}