diff --git a/test/helpers/providers/claude.ts b/test/helpers/providers/claude.ts index ce77767c2..858a30e0d 100644 --- a/test/helpers/providers/claude.ts +++ b/test/helpers/providers/claude.ts @@ -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 };