Skip to content

claude-cli auth-epoch flips on token rotation, forcing session resets mid-conversation #74312

Description

@aderius

Summary

On macOS, long-running Claude CLI agent sessions are hard-reset every few minutes during active conversations. The reset is triggered by cli session reset: reason=auth-epoch whenever Claude CLI rotates its OAuth tokens (which it does periodically and especially aggressively near token expiry). The user's live conversation context is dropped on every rotation.

Symptom

Gateway log shows clusters of:

[agent/cli-backend] cli session reset: provider=claude-cli reason=auth-epoch

Frequency correlates with OAuth token refresh activity. Observed cadence on one host: ~16 resets in 24h, accelerating to 2 resets in 3 minutes during the hour leading up to token expiry.

Example log slice (auth-epoch reset preceded by keychain re-read):

2026-04-29T07:07:55.048-04:00 [agents/auth-profiles] read anthropic credentials from claude cli keychain
2026-04-29T07:07:55.495-04:00 [agent/cli-backend] cli session reset: provider=claude-cli reason=auth-epoch
2026-04-29T07:10:04.989-04:00 [agent/cli-backend] cli session reset: provider=claude-cli reason=auth-epoch
2026-04-29T07:10:04.999-04:00 [agent/cli-backend] claude live session close: provider=claude-cli model=claude-opus-4-7 reason=restart

Root cause

resolveCliAuthEpoch (in dist/prepare.runtime-*.js) computes a sha256 of the Claude CLI credential. The hash uses encodeClaudeCredential:

function encodeClaudeCredential(credential) {
    if (credential.type === "oauth") return encodeOAuthIdentity(credential);
    return JSON.stringify([
        "token",
        credential.provider,
        credential.token   // rotating access token in the hash
    ]);
}

For OAuth credentials, encodeOAuthIdentity correctly hashes only stable identity fields (provider, clientId, email, accountId, etc). For token credentials, the rotating credential.token is included — so any token rotation flips the hash.

This is normally fine because Claude CLI OAuth credentials always parse as type: "oauth" (refresh token present). The bug is a race condition with parseClaudeCliOauthCredential in dist/store-*.js:

if (typeof refreshToken === "string" && refreshToken) return {
    type: "oauth", ...
};
return {
    type: "token", ...
};

The Claude CLI keychain blob on macOS contains both accessToken and refreshToken. When Claude CLI rewrites the keychain entry during a token rotation, a non-atomic read can briefly see a JSON missing refreshToken (or with empty string). The parser falls into type: "token", the new access token enters the auth-epoch hash, the hash flips, and the cli-backend forces reason=auth-epoch reset on every active session — dropping the user's live conversation.

The same race affects encodeAuthProfileCredential case "token" (also in dist/prepare.runtime-*.js), since the auth-profile sync from external CLI propagates the parsed credential type into the profile store.

On a Mac with no ~/.claude/.credentials.json fallback file (keychain-only), getLocalCliCredentialFingerprint returns undefined and the epoch is determined entirely by the auth-profile path — making the auth-profile branch the dominant trigger.

Suggested fix

Identity-only hash for both types, since rotating tokens shouldn't invalidate live sessions — only identity changes (account switch, logout/login) should:

function encodeClaudeCredential(credential) {
    return encodeOAuthIdentity(credential);
}

And encodeAuthProfileCredential case "token": drop credential.token from the hash, keep tokenRef/email/displayName.

Alternatively, harden parseClaudeCliOauthCredential to return the prior cached value on partial reads — but the identity-only hash is the more robust fix because:

  1. Token replacement (different token, same identity) shouldn't reset live sessions either — that's an authorized action, not an identity change.
  2. Defends against any future race conditions on keychain reads.

Environment

  • OpenClaw 2026.4.25
  • macOS Darwin 24.3.0 arm64
  • Claude CLI OAuth auth (Max 20x), keychain-only (no ~/.claude/.credentials.json)
  • Single Anthropic account, no auth-profile changes during the observation window

Workaround

Local patch applied at /opt/homebrew/lib/node_modules/openclaw/dist/prepare.runtime-*.js (backup at .bak.2026-04-29-auth-epoch). Will report back whether resets stop after gateway restart.

Metadata

Metadata

Assignees

No one assigned

    Labels

    staleMarked as stale due to inactivity

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions