Skip to content

[Bug]: "No credentials found for profile anthropic:claude-cli" fires in cron lane despite fresh keychain — #71026 fix not reaching runtime path #73682

Description

@RyanSandoval

Summary

In `2026.4.26`, the cron lane fails with `Error: No credentials found for profile "anthropic:claude-cli"` when the persisted `auth-profiles.json` token has expired, even though the macOS Keychain (`Claude Code-credentials`) holds fresh OAuth tokens. This is despite `EXTERNAL_CLI_SYNC_PROVIDERS` including `CLAUDE_CLI_PROFILE_ID` (the supposed fix from #71026, commit `8bbb143ab87e`).

The original #71026 was about a stale status display; this is an actual runtime failure — the cron throws and model-fallback dead-ends (`reason=auth next=none`).

Repro

  1. Let the persisted `~/.openclaw/agents/main/agent/auth-profiles.json` `anthropic:claude-cli.expires` go past now (i.e., wait ~8h after the last `claude /login`).
  2. Keep `claude` CLI fresh — keychain `Claude Code-credentials` still has a valid `expiresAt`.
  3. Trigger a cron whose model resolves to provider `anthropic` (e.g., `anthropic/claude-haiku-4-5-20251001`) routed via `order.anthropic = ["anthropic:claude-cli"]`.

Observed

```
[diagnostic] lane task error: lane=cron-nested … error="Error: No credentials found for profile \"anthropic:claude-cli\"."
[model-fallback/decision] decision=candidate_failed reason=auth next=none detail=No credentials found for profile "anthropic:claude-cli".
```

`gateway.log` shows the gateway itself reads the keychain successfully on its own cadence (`[agents/auth-profiles] read anthropic credentials from claude cli keychain`), but the cron lane sub-context either doesn't reach that path or reaches it and the bootstrap is rejected silently.

Root cause hypothesis

`refreshOAuthCredential` in `oauth.ts` is a dead-end for `claude-cli` creds. The provider has no entry in pi-ai's OAuth registry (`getOAuthProviders()` only knows `anthropic`, `github-copilot`, `antigravity`, `gemini-cli`, `openai-codex`):

```ts
const oauthProvider = resolveOAuthProvider(credential.provider); // "claude-cli" → null
if (!oauthProvider || typeof getOAuthApiKey !== "function") return null;
```

So when the persisted credential is expired, the only escape is the keychain bootstrap. If that bootstrap fails for any reason in the cron sub-context (cache state, identity check, sub-process state), refresh returns `null`, `resolveApiKeyForProfile` returns `null`, and `resolveApiKeyForProvider` throws `No credentials found` — no fallback, no diagnostic.

Manual fix that worked

```bash

1. Read keychain

security find-generic-password -s "Claude Code-credentials" -w

2. Replace the anthropic:claude-cli profile in auth-profiles.json with {access, refresh, expires} from claudeAiOauth

3. Re-run cron → succeeds

```

I've also set up a launchd LaunchAgent to do this every 10 minutes as a workaround.

Suggested fix directions

  1. Don't make refresh a silent dead-end — when `refreshOAuthCredential` returns `null` for a provider that has no adapter, surface a clearer error and explicitly re-attempt the keychain bootstrap.
  2. Always prefer keychain at runtime on darwin for `claude-cli` — skip the "is local usable?" gate and adopt unconditionally when `EXTERNAL_CLI_SYNC_PROVIDERS` resolves fresh creds.
  3. Add diagnostic logging when the bootstrap path is skipped (currently silent — `log$1.debug` only).

Environment

  • OpenClaw `2026.4.26` (`be8c246`)
  • macOS 25.3.0 (Darwin)
  • Node `v22.16.0`
  • Claude CLI keychain auth (`subscriptionType: max`)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions