Skip to content

fix(models): reflect Claude CLI auth status#71332

Merged
obviyus merged 4 commits into
openclaw:mainfrom
neeravmakwana:fix/claude-cli-auth-status
Apr 26, 2026
Merged

fix(models): reflect Claude CLI auth status#71332
obviyus merged 4 commits into
openclaw:mainfrom
neeravmakwana:fix/claude-cli-auth-status

Conversation

@neeravmakwana

@neeravmakwana neeravmakwana commented Apr 25, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Problem: openclaw models status could report anthropic:claude-cli as expired or expiring from a stale stored profile while Claude CLI native credentials were fresh enough for runtime use.
  • Why it matters: users saw an auth health warning even though direct Claude CLI and OpenClaw Claude runtime paths still worked.
  • What changed: auth health now evaluates the known Claude CLI profile against fresh native Claude CLI credentials for status only, without overlaying those credentials into the general auth store.
  • What did NOT change (scope boundary): Claude CLI runtime execution, token refresh, credential persistence, provider selection policies, and plugin auth contracts are unchanged.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

Root Cause (if applicable)

  • Root cause: models status built OAuth health from stale stored profile expiry data, while Claude CLI runtime auth was resolved separately through the native Claude CLI credential path.
  • Missing detection / guardrail: there was no regression test proving anthropic:claude-cli health reflects fresh native Claude CLI credentials when the stored profile has drifted stale.
  • Contributing context (if known): persisted anthropic:claude-cli profiles can drift from local Claude CLI credentials after the external CLI refreshes independently.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file: src/agents/auth-health.test.ts, src/commands/models/list.status.test.ts
  • Scenario the test should lock in: anthropic:claude-cli health uses fresh native Claude CLI OAuth expiry for status while the status command keeps the no-external-profile store path.
  • Why this is the smallest reliable guardrail: it covers the health/status mismatch without invoking a real Claude CLI binary or changing runtime credential resolution.
  • Existing test that already covers this (if any): N/A
  • If no new test is added, why not: N/A

User-visible / Behavior Changes

openclaw models status now reflects fresh local Claude CLI OAuth credentials when the stored anthropic:claude-cli profile is stale.

Diagram (if applicable)

Before:
models status -> stored claude-cli profile expiry -> stale warning

After:
models status -> auth health checks native Claude CLI expiry for claude-cli -> current status

Security Impact (required)

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? No
  • If any Yes, explain risk + mitigation: N/A

Security/runtime controls unchanged: the fix reuses the existing Claude CLI credential reader only inside auth-health status evaluation and does not add an external auth profile, plugin contract, store overlay, persistence path, refresh path, network call, or command execution surface.

Repro + Verification

Environment

  • OS: macOS local dev
  • Runtime/container: Node 22 / pnpm workspace
  • Model/provider: Anthropic Claude CLI
  • Integration/channel (if any): CLI models status
  • Relevant config (redacted): stale anthropic:claude-cli stored profile plus fresh native Claude CLI OAuth credentials

Steps

  1. Have a stale stored anthropic:claude-cli profile.
  2. Have fresh local Claude CLI OAuth credentials.
  3. Run openclaw models status.

Expected

  • Status reflects the fresh Claude CLI native OAuth credential.

Actual

  • Before this fix, status could report the stale stored profile as expired or expiring.

Evidence

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Human Verification (required)

What I personally verified, and how:

  • Verified scenarios: auth health reports a fresh status for a stale anthropic:claude-cli stored profile when native Claude CLI OAuth credentials are fresh; models status keeps using the no-external-profile store path.
  • Edge cases checked: no external auth profile is added, so native Claude CLI credentials are not overlaid into AuthProfileStore and cannot be persisted by external-profile save filtering.
  • What I did not verify: live Claude CLI login against a real host credential file.

Exact tests run:

  • pnpm test src/agents/auth-health.test.ts src/commands/models/list.status.test.ts extensions/anthropic/index.test.ts
  • pnpm check:changed

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

AI-assisted: yes.

Compatibility / Migration

  • Backward compatible? Yes
  • Config/env changes? No
  • Migration needed? No
  • If yes, exact upgrade steps: N/A

Risks and Mitigations

  • Risk: Claude CLI health could diverge from stored profile health for the fixed anthropic:claude-cli profile.
    • Mitigation: the override is scoped to the known Claude CLI profile id and only affects health/status calculation; runtime auth resolution and persisted store contents are unchanged.

@greptile-apps

greptile-apps Bot commented Apr 25, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a false expiry/expiring warning in openclaw models status for anthropic:claude-cli users whose stored profile was stale but whose local Claude CLI OAuth credentials were still fresh. The fix has two parts: the Anthropic plugin now exposes live Claude CLI OAuth credentials as a runtime-only external auth profile via the new resolveExternalAuthProfiles hook, and modelsStatusCommand is switched to call ensureAuthProfileStore (which overlays external profiles) instead of the no-external-profiles variant.

Confidence Score: 5/5

Safe to merge — targeted, well-tested fix with no credential-persistence risk.

All changes are minimal and directly address the stated bug. The runtime-only persistence flag prevents any accidental write-back of native Claude CLI credentials into the stored auth profile. Tests properly isolate the two store functions and lock in the correct call path. No security or data-integrity concerns identified.

No files require special attention.

Reviews (1): Last reviewed commit: "fix(models): reflect Claude CLI auth sta..." | Re-trigger Greptile

@steipete

Copy link
Copy Markdown
Contributor

Codex review: do not land this until the runtime-only credential boundary is fixed.

I checked the current auth-profile code path and Aisle's persistence concern looks real. resolveExternalAuthProfiles returns the Claude CLI OAuth access and refresh token as persistence: "runtime-only", but ensureAuthProfileStore overlays that credential into store.profiles. Later saveAuthProfileStore filters through shouldPersistExternalAuthProfile, which delegates to shouldPersistRuntimeExternalOAuthProfile; for runtime-only profiles it returns true whenever the credential differs from the external source. A token refresh can therefore turn a runtime-only Claude CLI credential into a persisted auth-profiles.json entry.

The broad store overlay is also too much for a status command: this PR switches models status from the no-external-profiles store to the full external overlay, making raw Claude CLI OAuth material part of the general auth store surface just to display freshness.

Suggested safer shape: keep models status on the persisted store, and add a narrow Anthropic/Claude-CLI status probe that reads the local CLI credential only for freshness display, without overlaying the refresh token into AuthProfileStore and without allowing runtime-only profiles to persist after refresh. At minimum, add regression coverage that a refreshed runtime-only external profile is never written to disk.

@neeravmakwana
neeravmakwana requested a review from a team as a code owner April 25, 2026 01:43
@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling and removed extensions: anthropic labels Apr 25, 2026
@neeravmakwana

Copy link
Copy Markdown
Contributor Author

Addressed the AI review findings in commit dfda286d51.

What changed:

  • Removed the Anthropic resolveExternalAuthProfiles approach entirely.
  • Removed the externalAuthProviders manifest contract addition.
  • Kept models status on the no-external-profiles auth-store path.
  • Moved the fix into buildAuthHealthSummary, scoped only to anthropic:claude-cli, where native Claude CLI credentials are read only to compute status/expiry.
  • Added a regression test showing a stale stored anthropic:claude-cli profile reports fresh health when native Claude CLI OAuth credentials are fresh.

Review findings:

  • Aisle fix: add @lid format support and allowFrom wildcard handling #1, runtime-only external OAuth tokens could be persisted after refresh: addressed by removing the runtime-only external profile entirely. There is no new external-profile persistence path in the final diff.
  • Aisle Login fails with 'WebSocket Error (socket hang up)' ECONNRESET #2, Claude CLI tokens exposed through external profiles/store overlay: addressed by removing the external profile and leaving raw Claude CLI credentials out of AuthProfileStore overlays. Status health uses the existing reader locally and does not serialize or overlay the credential into the store.
  • Aisle WA business, groups & office hours  #3, credential file loading hardening: not changed here because the final fix does not introduce a new credential file loader or new trust boundary. It reuses the existing Claude CLI credential reader with allowKeychainPrompt: false; no new network, command execution, or persistence behavior is added.
  • Aisle Images not passed to Claude CLI - only path reference in text #4, external profile ID collision/substitution: addressed by removing the external profile hook and manifest contract. The final diff does not add any external profile that can collide with persisted auth profiles.
  • Greptile: no code changes were requested, but its earlier summary described the now-removed external-profile implementation. The PR body has been updated to describe the final status-only health approach.

Security/runtime controls unchanged:

  • Claude CLI runtime execution remains on the existing synthetic/native runtime path.
  • Token refresh behavior is unchanged.
  • auth-profiles.json persistence behavior is unchanged.
  • No plugin auth contract is added.
  • No external auth profile overlay is added.
  • No new network call or command/tool execution surface is added.

Tests run:

  • pnpm test src/agents/auth-health.test.ts src/commands/models/list.status.test.ts extensions/anthropic/index.test.ts
  • pnpm check:changed

@neeravmakwana

Copy link
Copy Markdown
Contributor Author

I will pause work on this given Peter's comment above about runtime-only credential boundary

@aisle-research-bot

aisle-research-bot Bot commented Apr 26, 2026

Copy link
Copy Markdown

🔒 Aisle Security Analysis

We found 2 potential security issue(s) in this PR:

# Severity Title
1 🟠 High Synthetic auth credential (API key/token) included in models status --json output
2 🟡 Medium Untrusted plugin synthetic auth can spoof OAuth/token health status in models status output
1. 🟠 Synthetic auth credential (API key/token) included in `models status --json` output
Property Value
Severity High
CWE CWE-532
Location src/commands/models/list.status-command.ts:249-300

Description

The models status command now resolves provider synthetic auth via plugins and stores the returned secret in syntheticAuthByProvider as credential: resolved?.apiKey.

Although the human-readable output only prints value and source, the JSON path (--json) serializes providerAuth objects which include syntheticAuth (passed through resolveProviderAuthOverview). Because resolveProviderAuthOverview returns syntheticAuth: params.syntheticAuth without filtering, any extra fields on that object (including credential, mode, expiresAt) will be included in JSON output.

Impact:

  • Any runtime plugin-provided API key/bearer token can be printed to stdout when users run models status --json.
  • This can leak secrets into terminal scrollback, logs, CI artifacts, bug reports, and telemetry collectors.

Vulnerable code paths:

  • Secret is captured from plugin: credential: resolved?.apiKey
  • Object is passed through to output: syntheticAuth: syntheticAuthByProvider.get(provider) and later writeRuntimeJson(... providers: providerAuth ...).

Vulnerable code:

syntheticAuthByProvider.set(normalized, {
  value: "plugin-owned",
  source: resolved?.source ?? "plugin synthetic auth",
  credential: resolved?.apiKey,
  mode: resolved?.mode,
  expiresAt: resolved?.expiresAt,
});
...
resolveProviderAuthOverview({
  ...,
  syntheticAuth: syntheticAuthByProvider.get(provider),
});
...
writeRuntimeJson(runtime, { auth: { providers: providerAuth, ... } });

Recommendation

Do not serialize raw credentials in models status output.

Options:

  1. Strip secret fields before passing syntheticAuth into resolveProviderAuthOverview / JSON payload.
  2. Or change resolveProviderAuthOverview to explicitly pick/return only safe fields.

Example fix (strip at call site):

const synthetic = syntheticAuthByProvider.get(provider);
resolveProviderAuthOverview({
  provider,
  cfg,
  store,
  modelsPath,
  syntheticAuth: synthetic
    ? { value: synthetic.value, source: synthetic.source }
    : undefined,
});

If you need to indicate presence of a credential, output a boolean or a masked value (e.g., via existing maskApiKey()), never the raw token/key.

2. 🟡 Untrusted plugin synthetic auth can spoof OAuth/token health status in models status output
Property Value
Severity Medium
CWE CWE-345
Location src/agents/auth-health.ts:116-117

Description

models status now passes plugin-provided synthetic auth material into buildAuthHealthSummary() via runtimeCredentialsByProvider. buildProfileHealth() then prefers the runtime credential over the stored credential when computing health.

This creates a trust-boundary issue:

  • Input: resolveProviderSyntheticAuthWithPlugin() returns mode, expiresAt, and apiKey/token data controlled by a plugin.
  • Transformation: syntheticAuthCredential() converts that plugin result into an AuthProfileCredential.
  • Sink / impact: buildProfileHealth() uses runtimeCredential ?? credential for expiry/status calculations, which can cause an expired or missing stored profile to be reported as ok in the "OAuth/token status" section.
  • The OAuth/token status output does not indicate that the health was derived from plugin/runtime synthetic auth rather than the stored profile, enabling operator deception/social engineering (e.g., a plugin can claim long-lived freshness by setting expiresAt far in the future).

Vulnerable flow excerpts:

  • Plugin result -> runtime credential map
const resolved = resolveProviderSyntheticAuthWithPlugin({ /* plugin-controlled */ });
...
const runtimeCredentialsByProvider = new Map(
  Array.from(syntheticAuthByProvider.entries())
    .map(([provider, auth]) => [provider, syntheticAuthCredential(provider, auth)] as const)
    .filter((entry): entry is readonly [string, AuthProfileCredential] => Boolean(entry[1])),
);
  • Runtime credential overrides stored credential for health calculation
const healthCredential = runtimeCredential ?? credential;

Because plugins are not necessarily trusted to provide truthful metadata, the status view can be made to show healthy auth when canonical stored auth is unhealthy.

Recommendation

Treat plugin/runtime synthetic auth as separate from stored profile health, or clearly label it.

Options:

  1. Do not override stored profile health with runtime credentials. Instead, report runtime synthetic auth as an additional status field:
// buildProfileHealth: always compute from stored credential
const provider = normalizeProviderId(credential.provider);
...
​// separately attach runtime health if present
const runtime = runtimeCredential ? computeHealth(runtimeCredential) : undefined;
return { ...storedHealth, runtime };
  1. If overriding is intentional for models status, add an explicit marker in AuthProfileHealth and in the CLI output:
  • e.g. healthSource: 'stored' | 'runtime_plugin'
  • render: ok (runtime plugin) / expired (stored)
  1. Consider constraining what plugins can claim:
  • Ignore plugin-provided expiresAt unless verified by an actual probe/refresh.
  • Prefer canonical store state for "OAuth/token status" while using plugin synthetic auth only for the provider-level "effective" overview.

Analyzed PR: #71332 at commit 3c33b31

Last updated on: 2026-04-26T03:51:01Z

@obviyus
obviyus force-pushed the fix/claude-cli-auth-status branch from 7b3f707 to f805450 Compare April 26, 2026 03:40
@obviyus obviyus self-assigned this Apr 26, 2026

@obviyus obviyus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified the models status/Claude CLI mismatch: status now reads runtime synthetic-auth expiry instead of hardcoding Claude credential reads into core auth health.

Maintainer follow-up: moved the changelog entry into the active top Fixes block and kept the provider-owned synthetic auth seam.

Local gate: focused auth/status/Anthropic tests passed; check:changed passed type/lint/import-cycle/guards, with one later Vitest worker-pool infrastructure failure in changed tests after the changelog-only commit.

@obviyus
obviyus merged commit acd3d2b into openclaw:main Apr 26, 2026
64 of 65 checks passed
@obviyus

obviyus commented Apr 26, 2026

Copy link
Copy Markdown
Contributor

Landed on main.

Thanks @neeravmakwana.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling commands Command implementations extensions: anthropic size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Claude CLI auth status can show expired/expiring even when local Claude credentials are fresh and runtime works

3 participants