Skip to content

Plugin can't read raw 'Claude Code' Keychain service (Anthropic console API key) #235

Description

@hypeitnow

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions