fix(model-benchmark): recognize macOS Keychain auth in the claude adapter (#1890)

The default macOS Claude Code install stores OAuth under the Keychain
generic-password service "Claude Code-credentials" and never writes
~/.claude/.credentials.json, so available()'s file-or-env sniff reported
"No Claude auth found" while claude -p worked fine. On darwin the sniff
now also probes the Keychain entry — metadata only (no -w, the secret is
never read), 5s timeout, any security(1) failure degrades to not-found.

Verified live on this machine (subscription install, no creds file,
Keychain entry present).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Garry Tan 2026-08-14 12:02:28 -07:00
parent 4d15254229
commit cd5433add1
No known key found for this signature in database
GPG Key ID: C1F69E85C74EFE1D
1 changed files with 22 additions and 3 deletions

View File

@ -1,6 +1,6 @@
import type { ProviderAdapter, RunOpts, RunResult, AvailabilityCheck } from './types';
import { estimateCostUsd } from '../pricing';
import { execFileSync } from 'child_process';
import { execFileSync, spawnSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
@ -25,11 +25,30 @@ export class ClaudeAdapter implements ProviderAdapter {
if (!resolved) {
return { ok: false, reason: 'claude CLI not found on PATH. Install from https://claude.ai/download or npm i -g @anthropic-ai/claude-code (or set GSTACK_CLAUDE_BIN)' };
}
// Auth sniff: ~/.claude/.credentials.json OR ANTHROPIC_API_KEY
// Auth sniff: ~/.claude/.credentials.json OR ANTHROPIC_API_KEY OR (macOS)
// the Keychain entry subscription installs use instead of the creds file.
// #1890: the default macOS install stores OAuth under the generic-password
// service "Claude Code-credentials" and never writes .credentials.json,
// so the file-or-env sniff reported "No Claude auth found" while
// `claude -p` worked fine. Metadata probe only (no -w — never reads the
// secret), and any failure of `security` itself falls through to the
// not-found reason rather than throwing.
const credsPath = path.join(os.homedir(), '.claude', '.credentials.json');
const hasCreds = fs.existsSync(credsPath);
const hasKey = !!process.env.ANTHROPIC_API_KEY;
if (!hasCreds && !hasKey) {
let hasKeychain = false;
if (!hasCreds && !hasKey && process.platform === 'darwin') {
try {
const probe = spawnSync('security', ['find-generic-password', '-s', 'Claude Code-credentials'], {
stdio: 'ignore',
timeout: 5000,
});
hasKeychain = probe.status === 0;
} catch {
hasKeychain = false;
}
}
if (!hasCreds && !hasKey && !hasKeychain) {
return { ok: false, reason: 'No Claude auth found. Log in via `claude` interactive session, or export ANTHROPIC_API_KEY.' };
}
return { ok: true };