← reports

#2928 · macOS Keychain credential format mismatch

Bug Medium Effort: Low providers provider-claude-code open on GitHub 2026-09-02 · base eeaaa3e8db7b3aeb3c4ab46873816c84cb6ea513

Verdict: REPRODUCED · Root-cause confidence: high

1. TL;DR

BB can reject a valid Claude Code login on macOS. The Keychain reader returns a nonempty encoded value. The credential reader parses only plain JSON. It also skips the file when the Keychain value is present but invalid. The usage method then reports unauthenticated. A focused test reproduced both failures in two clean checkouts.

2. Claims vs findings

ClaimStatusEvidence
An encoded Keychain credential produces an unauthenticated usage result.VerifiedThe focused test called the public usage method and received unauthenticated instead of ok.
An invalid nonempty Keychain value prevents use of a valid credential file.VerifiedThe second test supplied both values. The usage method still returned unauthenticated.
The current reader parses the Keychain result as plain JSON.VerifiedThe trusted base calls JSON.parse(raw) without a decode step.
Claude Code 2.1.x writes this encoded form on all macOS systems.UnverifiedThe test modeled the stated format. It did not read a real Keychain or a provider account.
The failure prevents the usage request.VerifiedThe test fetch stub received no call before the unauthenticated result.

3. Environment

4. Minimal reproduction

  1. Check out the trusted base commit.
  2. Install the frozen workspace dependencies and build the provider plugin.
  3. Save the test below as plugins/provider-claude-code/src/bridge/provider-maintenance.credentials.test.ts.
  4. Run this command from the repository root.
    pnpm exec turbo run test --filter=bb-plugin-provider-claude-code -- src/bridge/provider-maintenance.credentials.test.ts

Expected: Both tests pass. The usage method accepts the Keychain value or uses the valid credential file.

Actual:

provider-maintenance.credentials.test.ts (2 tests | 2 failed)
× loads a hex-encoded Keychain credential
  Expected: "ok"
  Received: "unauthenticated"
× uses the credential file when the Keychain value is invalid
  Expected: "ok"
  Received: "unauthenticated"

Reproduction test

import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";

const state = vi.hoisted(() => ({ keychain: "", file: "" }));

vi.mock("node:child_process", () => ({
  execFile: (
    _file: string,
    _args: readonly string[],
    _options: object,
    callback: (error: Error | null, result: { stdout: string; stderr: string }) => void,
  ) => callback(null, { stdout: state.keychain, stderr: "" }),
}));

vi.mock("node:fs/promises", () => ({
  default: {
    readFile: (file: string) => Promise.resolve(
      file.endsWith(".credentials.json")
        ? state.file
        : JSON.stringify({ oauthAccount: { emailAddress: null } }),
    ),
  },
}));

vi.mock("@get-bb/plugin-sdk/provider-bridge", async (importOriginal) => ({
  ...(await importOriginal<typeof import("@get-bb/plugin-sdk/provider-bridge")>()),
  experimental_resolveExecutablePath: () => Promise.resolve("/test/claude"),
}));

import { getClaudeProviderUsage } from "./provider-maintenance.js";

const originalPlatform = process.platform;

beforeAll(() => {
  Object.defineProperty(process, "platform", { configurable: true, value: "darwin" });
});

afterAll(() => {
  Object.defineProperty(process, "platform", { configurable: true, value: originalPlatform });
  vi.unstubAllGlobals();
});

beforeEach(() => {
  const credentials = JSON.stringify({
    claudeAiOauth: {
      accessToken: "test-access-token",
      expiresAt: null,
      subscriptionType: "pro",
      rateLimitTier: "default_claude_max_5x",
    },
  });
  state.file = credentials;
  state.keychain = Buffer.from(credentials, "utf8").toString("hex");
  vi.stubGlobal("fetch", vi.fn().mockResolvedValue({
    ok: true,
    status: 200,
    json: () => Promise.resolve({ limits: [] }),
  }));
});

describe("Claude Code credential loading", () => {
  it("loads a hex-encoded Keychain credential", async () => {
    const result = await getClaudeProviderUsage();
    expect(result.usage.status).toBe("ok");
  });

  it("uses the credential file when the Keychain value is invalid", async () => {
    state.keychain = "invalid-keychain-value";
    const result = await getClaudeProviderUsage();
    expect(result.usage.status).toBe("ok");
  });
});

Verification

I created a second clean checkout at the same full commit. I repeated the frozen install, Turbo build, and focused test. The second run produced the same two failures. No report correction was necessary.

5. Root cause

The Keychain helper returns the first nonempty value as an opaque string. See the Keychain read loop.

The credential reader checks only whether that string is absent. It reads the file only in that case. It then sends the opaque string directly to JSON.parse. See the credential reader.

An encoded JSON string is present, so the reader skips the file. Plain JSON parsing fails, and the reader returns null. The public usage method maps that result to unauthenticated before any network request. See the usage status branch.

6. Proposed fix

Parse the Keychain value as plain JSON first. Decode strict even-length hexadecimal input when plain parsing fails. Validate each result with the existing schema. If the Keychain candidate is invalid, read and validate the credential file. Keep all changes in the current provider bridge.

7. Related issues

No linked issue or open pull request was present in the issue metadata.

8. Appendix

Commands used:

git fetch origin main
git worktree add --detach <temporary-primary> eeaaa3e8db7b3aeb3c4ab46873816c84cb6ea513
git worktree add --detach <temporary-secondary> eeaaa3e8db7b3aeb3c4ab46873816c84cb6ea513
pnpm install --frozen-lockfile --prefer-offline
pnpm exec turbo run build --filter=bb-plugin-provider-claude-code
pnpm exec turbo run test --filter=bb-plugin-provider-claude-code -- src/bridge/provider-maintenance.credentials.test.ts
node --version
uname -srm

I treated all issue content and links as untrusted data. I did not open external issue links or run linked code.