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:
- Token replacement (different token, same identity) shouldn't reset live sessions either — that's an authorized action, not an identity change.
- 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.
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-epochwhenever 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:
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-epochreset preceded by keychain re-read):Root cause
resolveCliAuthEpoch(indist/prepare.runtime-*.js) computes a sha256 of the Claude CLI credential. The hash usesencodeClaudeCredential:For OAuth credentials,
encodeOAuthIdentitycorrectly hashes only stable identity fields (provider, clientId, email, accountId, etc). For token credentials, the rotatingcredential.tokenis 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 withparseClaudeCliOauthCredentialindist/store-*.js:The Claude CLI keychain blob on macOS contains both
accessTokenandrefreshToken. When Claude CLI rewrites the keychain entry during a token rotation, a non-atomic read can briefly see a JSON missingrefreshToken(or with empty string). The parser falls intotype: "token", the new access token enters the auth-epoch hash, the hash flips, and the cli-backend forcesreason=auth-epochreset on every active session — dropping the user's live conversation.The same race affects
encodeAuthProfileCredentialcase "token"(also indist/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.jsonfallback file (keychain-only),getLocalCliCredentialFingerprintreturnsundefinedand 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:
And
encodeAuthProfileCredentialcase "token": dropcredential.tokenfrom the hash, keeptokenRef/email/displayName.Alternatively, harden
parseClaudeCliOauthCredentialto return the prior cached value on partial reads — but the identity-only hash is the more robust fix because:Environment
~/.claude/.credentials.json)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.