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
- 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`).
- Keep `claude` CLI fresh — keychain `Claude Code-credentials` still has a valid `expiresAt`.
- 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
- 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.
- 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.
- 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`)
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
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
Environment