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
expirestimestamp: 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
3490Date 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:
- Tier 1 β Local Profile: Stored in
~/.openclaw/agents/<agent>/agent/auth-profiles.json. Used as the primary credential source during auth resolution. - Tier 2 β External CLI Bootstrap: Fallback mechanism in
resolveExternalCliAuthProfiles()that reads source credentials from external CLI config files (e.g.,~/.claude/.credentials.jsonfor Claude CLI).
Failure Sequence
- 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 |
- External CLI Auto-Refresh: The source CLI (e.g., Claude CLI) independently refreshes its OAuth tokens every 1-2 days, updating
~/.claude/.credentials.jsonwith newaccessToken,refreshToken, andexpiresAtvalues. - 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.
- Auth Resolution Path: When
resolveExternalCliAuthProfiles()is invoked (per agent invocation, LLM call, or tool call), it detects the local profile'sexpiresfield 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
expiresfield 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
Recommended Approach: Option A (Auto-Sync on Detection)
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 ceaseStep 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 successfullyAutomation 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 close3. 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-cli5. 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 communication6. 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
jqAvailability: Thejqcommand is required for JSON manipulation. Windows users must install it separately (via Chocolatey:choco install jqor scoop) or use PowerShell's built-inConvertFrom-Json/ConvertTo-Jsoncmdlets. - 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
onboardBehavior: Runningopenclaw onboard --auth-choice anthropic-clidoes NOT refresh just the profile. It also overwritesagents.defaults.agentRuntime.idandagents.defaults.model.primary. Use this command only for full reconfiguration, not profile refreshes. - Symlink Pitfalls: If
~/.openclawor~/.claudeare symlinks pointing to different filesystems, file modification timestamps may not behave as expected. Verify withls -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. Usedate -ufor UTC comparisons. - JSON Date Precision: The external CLI may store
expiresAtwith 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-cliprovider but have separate profile files, each must be updated independently. The fix (Option A) should iterate all agent profile files. bootstrapOnly: trueProfiles: Some profiles (likeopenai-codexin the reported case) havebootstrapOnly: truewhich 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
INFOorWARN.
π Related Errors
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
expirestimestamp wrong, but therefreshtoken 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()