May 11, 2026 β€’ Version: 2026.5.6

Stale OAuth Profile Log Flood: External CLI Auth Never Auto-Resyncs

OpenClaw local oauth profiles bootstrapped from external CLIs (e.g., anthropic:claude-cli) become stale after source CLI refreshes, causing high-volume DEBUG log flooding without functional failure.


πŸ” Symptoms

Overview

OpenClaw maintains local OAuth profiles in ~/.openclaw/agents/<agent>/agent/auth-profiles.json for each authentication provider. When a profile derives from an external CLI (such as anthropic:claude-cli bootstrapping from Claude CLI’s credentials), the local copy is populated at initial onboarding but never synchronized when the source CLI refreshes its own OAuth tokens.

This manifests as a persistent, high-volume DEBUG log flood that masks other diagnostic signals in non-trivial deployments.

Technical Manifestations

  • Expired local expires timestamp: The local profile's expiration date falls into the past while the source CLI credentials remain valid.
  • DEBUG log flood: Every auth-resolution event triggers the message: "used external cli oauth bootstrap because local oauth was missing or unusable"
  • No functional failure: The system bootstraps successfully via fallback; authentication continues to work despite the staleness.

CLI Execution Examples

Inspecting the stale local profile:

$ cat ~/.openclaw/agents/main/agent/auth-profiles.json | jq '.profiles["anthropic:claude-cli"]'
{
  "access": "ya29....",
  "refresh": "1//0g...",
  "expires": "2026-05-07T14:30:00.000Z"
}

Inspecting the fresh source CLI credentials:

$ cat ~/.claude/.credentials.json | jq '.claudeAiOauth'
{
  "accessToken": "ya29....",
  "refreshToken": "1//0g...",
  "expiresAt": "2026-05-09T18:22:11.432Z"
}

Gateway log flood observation:

$ tail -f ~/.openclaw/logs/gateway.log | grep -i "external cli oauth bootstrap"
[2026-05-09T08:15:32.421Z] DEBUG [AuthResolver] used external cli oauth bootstrap because local oauth was missing or unusable
[2026-05-09T08:15:34.103Z] DEBUG [AuthResolver] used external cli oauth bootstrap because local oauth was missing or unusable
[2026-05-09T08:15:35.887Z] DEBUG [AuthResolver] used external cli oauth bootstrap because local oauth was missing or unusable
... (repeated for every agent/LLM/tool invocation)

Log volume metrics (production observation 2026-05-09):

$ grep -c "external cli oauth bootstrap" ~/.openclaw/logs/gateway.log
3490

Date comparison confirms staleness:

$ date -u
Thu May  9 2026 08:15:00 UTC

$ cat ~/.openclaw/agents/main/agent/auth-profiles.json | jq -r '.profiles["anthropic:claude-cli"].expires'
2026-05-07T14:30:00.000Z

# The local profile expired 2 days ago; source CLI is still valid

🧠 Root Cause

Architecture Overview

OpenClaw implements a two-tier OAuth profile system:

  1. Tier 1 β€” Local Profile: Stored in ~/.openclaw/agents/<agent>/agent/auth-profiles.json. Used as the primary credential source during auth resolution.
  2. Tier 2 β€” External CLI Bootstrap: Fallback mechanism in resolveExternalCliAuthProfiles() that reads source credentials from external CLI config files (e.g., ~/.claude/.credentials.json for Claude CLI).

Failure Sequence

  1. Initial Bootstrap: During openclaw onboard, the external CLI credentials are read and written into the local profile with field-name translation:
Source CLI Key→Local Profile Key
accessToken→access
refreshToken→refresh
expiresAt→expires
  1. External CLI Auto-Refresh: The source CLI (e.g., Claude CLI) independently refreshes its OAuth tokens every 1-2 days, updating ~/.claude/.credentials.json with new accessToken, refreshToken, and expiresAt values.
  2. Local Profile Staleness: OpenClaw's local profile retains the original tokens and expiration timestamp β€” it is never updated to reflect the source CLI's refresh.
  3. Auth Resolution Path: When resolveExternalCliAuthProfiles() is invoked (per agent invocation, LLM call, or tool call), it detects the local profile's expires field is in the past and logs the DEBUG bootstrap message before falling back to reading the external CLI credentials.

Code Path Analysis

The problematic logic resides in dist/store-DL6VwwSr.js:686 (OpenClaw 2026.5.6):

// Pseudocode representation of resolveExternalCliAuthProfiles()
function resolveExternalCliAuthProfiles(provider, agentId) {
  const localProfile = loadLocalProfile(agentId, provider);
  
  // Check if local profile is usable
  if (localProfile && !isExpired(localProfile.expires)) {
    // Profile is valid β€” use it directly
    return localProfile;
  }
  
  // Local profile is stale or missing β€” bootstrap from external CLI
  const externalProfile = readExternalCliCredentials(provider);
  
  if (externalProfile && !isExpired(externalProfile.expiresAt)) {
    // DEBUG: Fires on EVERY invocation when local profile is stale
    logger.debug("used external cli oauth bootstrap because local oauth was missing or unusable");
    
    // NOTE: External profile is returned but NOT written back to local profile
    return translateExternalToLocal(externalProfile);
  }
  
  return null;
}

The critical gap: the function returns the fresh external credentials but never persists them back to the local profile. This creates a perpetual staleness loop where the local profile is always outdated by 1-2 days.

Why Log Flood Occurs

The DEBUG message fires per-poll because:

  • The local profile expires field is permanently in the past (until manual intervention)
  • Every auth-resolution event triggers the same stale-local β†’ fresh-external β†’ log β†’ return path
  • High-volume deployments with frequent agent/LLM/tool invocations amplify the flood linearly

Why Functional Behavior Remains Correct

The system works because the fallback mechanism successfully reads and uses the fresh external CLI credentials. The DEBUG message is technically accurate (“used external cli oauth bootstrap”) but functionally misleading β€” it implies a problem when the bootstrap succeeds and authentication proceeds normally.

πŸ› οΈ Step-by-Step Fix

Modify resolveExternalCliAuthProfiles() to write fresh external credentials back to the local profile when detected as fresher.

Before (problematic behavior)

// dist/store-DL6VwwSr.js ~line 686
if (externalProfile && !isExpired(externalProfile.expiresAt)) {
  logger.debug("used external cli oauth bootstrap because local oauth was missing or unusable");
  return translateExternalToLocal(externalProfile);  // ❌ Never persists
}

After (fix applied)

// dist/store-DL6VwwSr.js ~line 686
if (externalProfile && !isExpired(externalProfile.expiresAt)) {
  // Compare timestamps before deciding to update
  const localExpiry = localProfile ? new Date(localProfile.expires) : null;
  const externalExpiry = new Date(externalProfile.expiresAt);
  
  if (!localProfile || externalExpiry > localExpiry) {
    // External credentials are fresher β€” persist them locally
    const translatedProfile = translateExternalToLocal(externalProfile);
    persistLocalProfile(agentId, provider, translatedProfile);
    logger.debug("synced external cli oauth to local profile");
  }
  
  return translateExternalToLocal(externalProfile);
}

Manual Workaround (Immediate Relief)

For deployments unable to wait for the fix release, perform a one-time manual sync:

Step 1: Backup the existing local profile

cp ~/.openclaw/agents/main/agent/auth-profiles.json \
   ~/.openclaw/agents/main/agent/auth-profiles.json.backup-$(date +%Y%m%d-%H%M%S)

Step 2: Extract and translate credentials from source CLI

# Read fresh credentials from Claude CLI
EXTERNAL_CREDS="$HOME/.claude/.credentials.json"
ACCESS_TOKEN=$(cat "$EXTERNAL_CREDS" | jq -r '.claudeAiOauth.accessToken')
REFRESH_TOKEN=$(cat "$EXTERNAL_CREDS" | jq -r '.claudeAiOauth.refreshToken')
EXPIRES_AT=$(cat "$EXTERNAL_CREDS" | jq -r '.claudeAiOauth.expiresAt')

echo "Access Token: ${ACCESS_TOKEN:0:20}..."
echo "Expires At: $EXPIRES_AT"

Step 3: Update the local profile with field translation

# Using jq to update the specific profile in-place
PROFILE_FILE="$HOME/.openclaw/agents/main/agent/auth-profiles.json"

cat "$PROFILE_FILE" | jq --arg access "$ACCESS_TOKEN" \
                          --arg refresh "$REFRESH_TOKEN" \
                          --arg expires "$EXPIRES_AT" \
                          '.profiles["anthropic:claude-cli"] = {
                            "access": $access,
                            "refresh": $refresh,
                            "expires": $expires
                          }' > "${PROFILE_FILE}.tmp" && \
mv "${PROFILE_FILE}.tmp" "$PROFILE_FILE"

Step 4: Verify the update

$ cat ~/.openclaw/agents/main/agent/auth-profiles.json | jq '.profiles["anthropic:claude-cli"]'
{
  "access": "ya29....",
  "refresh": "1//0g...",
  "expires": "2026-05-09T18:22:11.432Z"
}

$ date -u
Thu May  9 2026 10:30:00 UTC

# βœ… expires is now in the future β€” log flood should cease

Step 5: Confirm log flood cessation (no restart required)

# The next auth-resolution event will read the updated file
# Observe that DEBUG messages for this provider cease
$ tail -f ~/.openclaw/logs/gateway.log | grep "anthropic:claude-cli"
[2026-05-09T10:31:05.221Z] DEBUG [AuthResolver] synced external cli oauth to local profile
[2026-05-09T10:31:05.223Z] DEBUG [AuthResolver] profile resolved successfully

Automation Script (Recurring Workaround)

For environments where the fix is not yet deployed, schedule this script via cron to run hourly:

#!/usr/bin/env bash
# sync-anthropic-oauth.sh β€” Scheduled sync for external CLI oauth profiles

set -euo pipefail

PROFILE_FILE="$HOME/.openclaw/agents/main/agent/auth-profiles.json"
EXTERNAL_CREDS="$HOME/.claude/.credentials.json"
PROFILE_KEY="anthropic:claude-cli"
LOG_FILE="${OPENCLAW_LOG_DIR:-"$HOME/.openclaw/logs"}/oauth-sync.log"

log() {
  echo "[$(date -u '+%Y-%m-%dT%H:%M:%SZ')] $1" >> "$LOG_FILE"
}

# Check if external credentials exist and are valid
if [[ ! -f "$EXTERNAL_CREDS" ]]; then
  log "WARN: External credentials file not found: $EXTERNAL_CREDS"
  exit 0
fi

EXPIRES_AT=$(cat "$EXTERNAL_CREDS" | jq -r '.claudeAiOauth.expiresAt // empty')
if [[ -z "$EXPIRES_AT" ]]; then
  log "WARN: No expiresAt found in external credentials"
  exit 0
fi

EXPIRES_EPOCH=$(date -d "$EXPIRES_AT" +%s 2>/dev/null) || exit 1
NOW_EPOCH=$(date +%s)

# Skip if external credentials are expired
if (( EXPIRES_EPOCH < NOW_EPOCH )); then
  log "INFO: External credentials expired at $EXPIRES_AT β€” skipping sync"
  exit 0
fi

# Read current local expiry
if [[ -f "$PROFILE_FILE" ]]; then
  LOCAL_EXPIRES=$(cat "$PROFILE_FILE" | jq -r --arg key "$PROFILE_KEY" \
                  '.profiles[$key].expires // empty')
  
  if [[ -n "$LOCAL_EXPIRES" ]]; then
    LOCAL_EPOCH=$(date -d "$LOCAL_EXPIRES" +%s 2>/dev/null) || true
    # Skip if local profile is already fresher or current
    if [[ -n "$LOCAL_EPOCH" ]] && (( LOCAL_EPOCH >= EXPIRES_EPOCH )); then
      log "DEBUG: Local profile is current β€” no sync needed"
      exit 0
    fi
  fi
fi

# Perform the sync
ACCESS_TOKEN=$(cat "$EXTERNAL_CREDS" | jq -r '.claudeAiOauth.accessToken')
REFRESH_TOKEN=$(cat "$EXTERNAL_CREDS" | jq -r '.claudeAiOauth.refreshToken')

BACKUP_FILE="${PROFILE_FILE}.backup-$(date +%Y%m%d-%H%M%S)"
cp "$PROFILE_FILE" "$BACKUP_FILE"

cat "$PROFILE_FILE" | jq --arg access "$ACCESS_TOKEN" \
                          --arg refresh "$REFRESH_TOKEN" \
                          --arg expires "$EXPIRES_AT" \
                          --arg key "$PROFILE_KEY" \
                          '.profiles[$key] = {
                            "access": $access,
                            "refresh": $refresh,
                            "expires": $expires
                          }' > "${PROFILE_FILE}.tmp" && \
mv "${PROFILE_FILE}.tmp" "$PROFILE_FILE"

log "INFO: Synced $PROFILE_KEY from external CLI (expires: $EXPIRES_AT)"

Cron entry for hourly execution:

0 * * * * /path/to/sync-anthropic-oauth.sh 2>> ~/.openclaw/logs/oauth-sync-cron.log

πŸ§ͺ Verification

Post-Fix Verification Steps

After applying the fix (either automated or manual), verify resolution using the following checks:

1. Confirm Local Profile Expiration is Future-Dated

$ cat ~/.openclaw/agents/main/agent/auth-profiles.json | jq -r '.profiles["anthropic:claude-cli"].expires'
2026-05-11T22:00:00.000Z

$ date -u
Thu May  9 2026 10:30:00 UTC

# βœ… expires (May 11) is AFTER current time (May 9)

2. Confirm Source CLI Credentials are Valid

$ cat ~/.claude/.credentials.json | jq -r '.claudeAiOauth.expiresAt'
2026-05-11T22:00:00.000Z

# βœ… Both timestamps should match or be very close

3. Verify Log Flood has Ceased

# Monitor for new instances of the bootstrap DEBUG message
$ tail -f ~/.openclaw/logs/gateway.log | grep -i "external cli oauth bootstrap"
# (Should produce no output after fix)

# Alternatively, count recent instances:
$ grep -c "external cli oauth bootstrap" ~/.openclaw/logs/gateway.log
3490  # Pre-fix count (unchanged historical data)

$ grep "external cli oauth bootstrap" ~/.openclaw/logs/gateway.log | tail -5
# (Should show no recent entries)

4. Verify Auth Resolution Uses Local Profile

# With debug logging, observe profile resolution path
$ tail -f ~/.openclaw/logs/gateway.log | grep -E "(profile resolved|synced external)"
[2026-05-09T10:31:05.223Z] DEBUG [AuthResolver] synced external cli oauth to local profile
[2026-05-09T10:31:05.225Z] DEBUG [AuthResolver] profile resolved successfully for anthropic:claude-cli

# Subsequent invocations should show no bootstrap messages
[2026-05-09T10:35:12.001Z] DEBUG [AuthResolver] profile resolved successfully for anthropic:claude-cli
[2026-05-09T10:40:45.332Z] DEBUG [AuthResolver] profile resolved successfully for anthropic:claude-cli

5. Functional Authentication Test

# Invoke a simple agent operation to confirm auth works
$ openclaw run --agent main "What time is it?" --max-turns 1

# Exit code 0 confirms successful authentication and LLM communication

6. Automated Verification Script

#!/usr/bin/env bash
# verify-oauth-fix.sh

PROFILE_FILE="$HOME/.openclaw/agents/main/agent/auth-profiles.json"
PROFILE_KEY="anthropic:claude-cli"

echo "=== OAuth Profile Staleness Verification ==="
echo ""

# Check local profile exists
if [[ ! -f "$PROFILE_FILE" ]]; then
  echo "❌ FAIL: Local profile file not found: $PROFILE_FILE"
  exit 1
fi

# Check profile exists
EXISTS=$(cat "$PROFILE_FILE" | jq -e ".profiles[\"$PROFILE_KEY\"]" >/dev/null 2>&1 && echo "yes" || echo "no")
if [[ "$EXISTS" != "yes" ]]; then
  echo "❌ FAIL: Profile '$PROFILE_KEY' not found in local profile file"
  exit 1
fi

# Check expiration
EXPIRES=$(cat "$PROFILE_FILE" | jq -r ".profiles[\"$PROFILE_KEY\"].expires")
EXPIRES_EPOCH=$(date -d "$EXPIRES" +%s 2>/dev/null) || { echo "❌ FAIL: Invalid expires format"; exit 1; }
NOW_EPOCH=$(date +%s)

echo "Current time (UTC): $(date -u '+%Y-%m-%dT%H:%M:%SZ')"
echo "Profile expires:    $EXPIRES"

if (( EXPIRES_EPOCH < NOW_EPOCH )); then
  echo "❌ FAIL: Profile is EXPIRED (expires is in the past)"
  exit 1
fi

echo "βœ… PASS: Profile expiration is in the future"

# Check for recent bootstrap log entries
LOG_FILE="$HOME/.openclaw/logs/gateway.log"
if [[ -f "$LOG_FILE" ]]; then
  # Get last hour's entries
  LAST_HOUR=$(date -u -d '1 hour ago' '+%Y-%m-%dT%H:%M')
  BOOTSTRAP_RECENT=$(grep "$LAST_HOUR" "$LOG_FILE" 2>/dev/null | grep -c "external cli oauth bootstrap" || echo "0")
  
  echo ""
  echo "Recent bootstrap log entries (last hour): $BOOTSTRAP_RECENT"
  if (( BOOTSTRAP_RECENT > 10 )); then
    echo "⚠️  WARN: Significant bootstrap activity detected β€” profile may still be stale"
  else
    echo "βœ… PASS: No excessive bootstrap logging"
  fi
fi

echo ""
echo "=== All Checks Passed ==="
$ chmod +x verify-oauth-fix.sh && ./verify-oauth-fix.sh
=== OAuth Profile Staleness Verification ===

Current time (UTC): 2026-05-09T10:45:00Z
Profile expires:    2026-05-11T22:00:00Z
βœ… PASS: Profile expiration is in the future

Recent bootstrap log entries (last hour): 0
βœ… PASS: No excessive bootstrap logging

=== All Checks Passed ===

⚠️ Common Pitfalls

Environment-Specific Traps

  • Windows Path Format: The credential and profile paths use POSIX-style paths in the issue. On Windows 11, translate ~ to %USERPROFILE% and use backslashes or forward slashes as appropriate: %USERPROFILE%\.openclaw\agents\main\agent\auth-profiles.json
  • PowerShell jq Availability: The jq command is required for JSON manipulation. Windows users must install it separately (via Chocolatey: choco install jq or scoop) or use PowerShell's built-in ConvertFrom-Json/ConvertTo-Json cmdlets.
  • Docker Container Isolation: When running OpenClaw inside Docker, ensure ~/.claude/ and ~/.openclaw/ are volume-mounted from the host. Otherwise, the container has its own credential store that never receives external CLI updates.

Configuration Missteps

  • Incorrect onboard Behavior: Running openclaw onboard --auth-choice anthropic-cli does NOT refresh just the profile. It also overwrites agents.defaults.agentRuntime.id and agents.defaults.model.primary. Use this command only for full reconfiguration, not profile refreshes.
  • Symlink Pitfalls: If ~/.openclaw or ~/.claude are symlinks pointing to different filesystems, file modification timestamps may not behave as expected. Verify with ls -la.
  • Permission Errors: Ensure the running user has read/write access to both the local profile file and the external CLI credential file. OpenClaw must be able to write updates back to the local profile.

Timestamp Interpretation Issues

  • Timezone Mismatch: OpenClaw stores all timestamps in ISO 8601 UTC format (2026-05-09T22:00:00.000Z). Ensure system clocks are synchronized. Use date -u for UTC comparisons.
  • JSON Date Precision: The external CLI may store expiresAt with millisecond precision while OpenClaw may truncate to seconds. This is acceptable β€” the important check is whether the timestamp is in the future.

Edge Cases

  • Multiple Agents with Same Provider: If two agents share the same anthropic:claude-cli provider but have separate profile files, each must be updated independently. The fix (Option A) should iterate all agent profile files.
  • bootstrapOnly: true Profiles: Some profiles (like openai-codex in the reported case) have bootstrapOnly: true which bypasses local profile lookup entirely. These will NOT exhibit the staleness issue or log flood. Do not confuse their absence of flood with a fix success.
  • External CLI Unavailable: If the source CLI (Claude CLI) is uninstalled or its credential file is deleted, the fallback mechanism will fail silently and authentication will use whatever stale tokens remain in the local profile (until they expire completely).

Debug Logging Overhead

  • Disk Space: With DEBUG logging enabled at logging.level: debug, the gateway log can grow rapidly in high-volume deployments. The 3,490-entry flood observed in one retention window demonstrates this. Configure log rotation to prevent disk exhaustion.
  • Log Level in Production: DEBUG-level logging is not recommended for production due to both volume and potential information disclosure. After resolving the staleness issue, consider raising log level to INFO or WARN.

Contextually Connected Issues

  • AUTH_PROFILE_EXPIRED (potential future error): If both the local profile and external CLI credentials expire before resolution occurs, authentication will fail. The staleness issue masks this failure mode until the external CLI also expires. Monitor for this error code if the external CLI has been offline for an extended period.
  • Regression #76562: Referenced as still active in OpenClaw 2026.5.6. Verify whether the staleness fix introduces any interactions with this regression or depends on a fix within it.
  • OAuth Refresh Token Rotation: When OAuth providers rotate refresh tokens on each use, the staleness issue becomes more severe β€” not only is the expires timestamp wrong, but the refresh token itself is stale and unusable. This would cause actual auth failures, not just log noise.

Historical Patterns

  • Bootstrap-Fallback Log Spam (General): Any auth provider that relies on external CLI bootstrapping without local persistence will exhibit similar behavior when the local profile is stale. The pattern is not unique to anthropic:claude-cli.
  • Credential File Permissions (Unix): Claude CLI stores credentials with mode 600 (-rw-------). If OpenClaw runs as a different user or via a service daemon, it may lack read access, causing the external CLI bootstrap to fail silently with an unreadable credential file.

Log Message Variations

The DEBUG message may appear in slightly different forms depending on the exact version:

# Possible variants to grep for during diagnosis:
"used external cli oauth bootstrap because local oauth was missing or unusable"
"external cli oauth bootstrap used for provider %s"
"bootstrap from external cli for profile %s β€” local unusable"
"oauth bootstrap: local profile stale, using external cli credentials"

External References

  • OpenClaw Auth Profiles: ~/.openclaw/agents/<agent>/agent/auth-profiles.json
  • Claude CLI Credentials: ~/.claude/.credentials.json
  • Gateway Log: ~/.openclaw/logs/gateway.log
  • Relevant Source: dist/store-DL6VwwSr.js:686 β€” resolveExternalCliAuthProfiles()

Evidence & Sources

This troubleshooting guide was automatically synthesized by the FixClaw Intelligence Pipeline from community discussions.