Resolved by @hypeitnow/opencode-claude-auth@1.6.14 (npm) / hypeitnow/opencode-claude-auth@e273493 (mirror main). The fix takes a different path than the originally-proposed one below — see comment for the full update. End-to-end verified on this Mac with haiku-4-5, sonnet-4-5, opus-4-1. 253/253 tests pass. Closing-ready; willing to push directly if push access is granted.
Original report
The claude CLI stores the Anthropic console API key as a raw sk-ant-api03-... string in the macOS Keychain service Claude Code (no JSON envelope, no -credentials suffix). This plugin only reads services matching Claude Code-credentials*, so on machines where the user signed in via claude auth login and got a console API key, the plugin prints:
No Claude Code credentials found. Running in API key mode with transform hook enabled.
Proposed fix (superseded)
Mirror with fix + tests + README update is published at:
hypeitnow/opencode-claude-auth main branch, tip c041ede (now e273493)
- 6 commits on top of
v1.5.4:
2713d40 feat: read raw 'Claude Code' Keychain entry as long-lived API key
ac9e774 build: add prepare script so bun install builds dist
9ea790a build: invoke tsc via node so prepare works without PATH lookup
df296ca build: portable build.mjs that resolves tsc via createRequire
c041ede build: commit pre-built dist so GitHub installs work without devDeps
f60dd76 Merge branch 'feat/claude-code-raw-keychain-fallback'
Diff summary (src/keychain.ts)
const RAW_KEY_SERVICE = "Claude Code"
const RAW_KEY_DEFAULT_TTL_MS = 365 * 24 * 60 * 60 * 1000
function parseRawApiKey(raw: string): ClaudeCredentials | null {
const trimmed = raw.trim()
if (!trimmed) return null
if (!trimmed.startsWith("sk-ant-")) return null
return {
accessToken: trimmed,
refreshToken: "",
expiresAt: Date.now() + RAW_KEY_DEFAULT_TTL_MS,
}
}
function parseCredentials(raw: string): ClaudeCredentials | null {
let parsed: unknown, parsedAsJson = true
try { parsed = JSON.parse(raw) }
catch { parsedAsJson = false; parsed = null }
if (!parsedAsJson) {
return parseRawApiKey(raw) // NEW fallback
}
// ...existing JSON path unchanged
}
listClaudeKeychainServices() also matches bare Claude Code (separate regex after the OAuth one).
writeBackCredentials() short-circuits (returns false) when source === "Claude Code" to avoid overwriting the raw key with a JSON envelope.
- 8 new tests added (4 parse, 3 service discovery, 1 writeback no-op). 47/47 keychain tests pass; 229/229 total.
Verified end-to-end
Plugin's readAllClaudeAccounts() against real macOS keychain on the mirror:
{
"accounts found": 1,
"label": "Claude",
"source": "Claude Code",
"accessPrefix": "sk-ant-api03-1R3Y_lxg2vmD...",
"refreshEmpty": true,
"expiresInDays": 365,
"subscriptionType": null
}
Why not a fork PR?
The repo was mirrored as a regular public repo (not via gh repo fork) because of EMU restrictions. GitHub's PR API rejects cross-repo PRs when neither side is a fork. Happy to push the branch to this repo directly if you grant push access, or to provide a .patch file / cherry-pick commands.
cc @griffinmartin
Original report
The
claudeCLI stores the Anthropic console API key as a rawsk-ant-api03-...string in the macOS Keychain serviceClaude Code(no JSON envelope, no-credentialssuffix). This plugin only reads services matchingClaude Code-credentials*, so on machines where the user signed in viaclaude auth loginand got a console API key, the plugin prints:Proposed fix (superseded)
Mirror with fix + tests + README update is published at:
hypeitnow/opencode-claude-authmainbranch, tipc041ede(nowe273493)v1.5.4:2713d40feat: read raw 'Claude Code' Keychain entry as long-lived API keyac9e774build: add prepare script so bun install builds dist9ea790abuild: invoke tsc via node so prepare works without PATH lookupdf296cabuild: portable build.mjs that resolves tsc via createRequirec041edebuild: commit pre-built dist so GitHub installs work without devDepsf60dd76Merge branch 'feat/claude-code-raw-keychain-fallback'Diff summary (
src/keychain.ts)listClaudeKeychainServices()also matches bareClaude Code(separate regex after the OAuth one).writeBackCredentials()short-circuits (returnsfalse) whensource === "Claude Code"to avoid overwriting the raw key with a JSON envelope.Verified end-to-end
Plugin's
readAllClaudeAccounts()against real macOS keychain on the mirror:{ "accounts found": 1, "label": "Claude", "source": "Claude Code", "accessPrefix": "sk-ant-api03-1R3Y_lxg2vmD...", "refreshEmpty": true, "expiresInDays": 365, "subscriptionType": null }Why not a fork PR?
The repo was mirrored as a regular public repo (not via
gh repo fork) because of EMU restrictions. GitHub's PR API rejects cross-repo PRs when neither side is a fork. Happy to push the branch to this repo directly if you grant push access, or to provide a.patchfile / cherry-pick commands.cc @griffinmartin