openclaw - ✅(Solved) Fix claude-cli auth-epoch flips on token rotation, forcing session resets mid-conversation [1 pull requests, 2 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#74312Fetched 2026-04-30 06:25:41
View on GitHub
Comments
2
Participants
2
Timeline
6
Reactions
2
Author
Timeline (top)
commented ×2cross-referenced ×2mentioned ×1subscribed ×1

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.

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.

Fix Action

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.

PR fix notes

PR #74493: fix(cli): identity-only auth-epoch hashing for token credentials (#74312)

Description (problem / solution / changelog)

Summary

  • Problem: Claude CLI sessions are hard-reset every few minutes during active conversations on macOS. Logs show cli session reset: provider=claude-cli reason=auth-epoch clusters that correlate with claude-cli's OAuth token rotation. Reporter (#74312) observed ~16 resets in 24h on one host, accelerating to 2 resets in 3 minutes near token expiry. The user's live conversation context is dropped on every rotation.
  • Why it matters: macOS Claude keychain rewrites are non-atomic. A read during rotation can briefly see accessToken without refreshToken, causing parseClaudeCliOauthCredential to return type: "token" instead of type: "oauth". The previous encodeClaudeCredential token branch hashed credential.token directly, so that transient race flipped the auth-epoch and reset every reusable claude-cli session. On macOS keychain-only setups (no ~/.claude/.credentials.json fallback), getLocalCliCredentialFingerprint returns undefined and the auth-profile branch is the dominant trigger; encodeAuthProfileCredential token case had the same hash-rotates-with-token defect.
  • What changed: Extends the identity-only hashing pattern Ayaan started in 1ff461fe7b (fix(cli): stabilize oauth session auth epochs) and 3eb6edc67c (fix(cli): key oauth session epochs on identity), and which Chunyue extended to Gemini in #71076, to the static-token credential branch. encodeClaudeCredential routes both OAuth and token claude-cli credentials through encodeOAuthIdentity, collapsing partial keychain reads onto the same provider-keyed identity hash. encodeAuthProfileCredential token case drops credential.token from the hash; identity fields (provider, tokenRef, email, displayName) are the discriminator. CLI_AUTH_EPOCH_VERSION bumped 4 → 5.
  • What did NOT change: parseClaudeCliOauthCredential (the parser, not hardened here per scope discipline; identity-only hashing makes the partial-read race harmless without parser changes), the OAuth branches (already identity-only since 3eb6edc67c), Gemini and Codex encoders (already identity-only), resolveCliAuthEpoch orchestration, or any consumer code (prepareExecution reads the same string).

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

  • Closes #74312
  • Related: 1ff461fe7b, 3eb6edc67c, #71076 (the OAuth + Gemini precedent)
  • This PR fixes a bug or regression

Root Cause

  • Root cause: encodeClaudeCredential and encodeAuthProfileCredential retained credential.token in the auth-epoch hash for the static-token branch, even though Ayaan's recent commits had moved OAuth credentials and Chunyue's #71076 had moved Gemini OAuth onto identity-only hashing. The remaining token branches hashed rotating material, so any transient type: "token" parse (which the macOS keychain rewrite race triggers reliably) flipped the epoch.
  • Missing detection / guardrail: The existing test asserted token rotation expect(second).not.toBe(first) — i.e., locked in the buggy behavior. There was no regression case proving OAuth and token claude-cli epochs match (which the partial-read race requires).
  • Contributing context: parseClaudeCliOauthCredential falls through to type: "token" when refreshToken is empty/missing. This is correct for credentials that were never OAuth, but produces false type: "token" parses during keychain rotation. The hash semantics needed to be stable across that fallthrough.

Regression Test Plan

  • 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/cli-auth-epoch.test.ts
  • Scenarios the tests lock in:
    • keeps claude cli token epochs stable across token rotation (replaces the previous changes ... when the static token changes assertion)
    • matches claude cli token and oauth epochs so partial keychain reads do not flip (new regression case for the partial-read race)
    • keeps token auth-profile epochs stable across credential.token rotation (auth-profile branch — the dominant trigger on keychain-only macOS setups)
    • changes token auth-profile epochs when the email identity changes (preserves real account-switch invalidation)

User-visible / Behavior Changes

  • Active claude-cli conversations no longer drop context every few minutes during token rotation.
  • cli session reset: reason=auth-epoch log lines no longer correlate with token rotation; they now only fire on real identity changes (provider, email, tokenRef, displayName).
  • CLI_AUTH_EPOCH_VERSION bumped 4 → 5; existing stored auth-epochs invalidate once on first read after upgrade (one expected reset per host on the upgrade boundary).
  • No config changes, no new directives.

Security Impact

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No (the hash is over identity fields; tokens are still read and used for auth requests, just no longer included in the epoch fingerprint)
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? No

Repro + Verification

Environment

  • OS: macOS (reporter on Ubuntu Kylin 20.04 — keychain race is macOS-specific but auth-profile branch hits other platforms via auth-profile sync)
  • Backend: claude-cli with OAuth credentials in macOS keychain

Steps (per reporter)

  1. Run a long-lived claude-cli agent on Telegram or another active channel.
  2. Monitor gateway.out for cli session reset: provider=claude-cli reason=auth-epoch lines.
  3. Observe that resets correlate with claude-cli OAuth token rotation activity (~every 30-60 minutes, accelerating near token expiry).

Expected (after this PR)

No auth-epoch resets during normal token rotation. Conversation context preserved across rotations.

Actual (before this PR)

Resets on every rotation. Each reset drops mid-conversation context. Reporter measured ~16 resets/24h, peaking at 2 resets in 3 minutes near token expiry.

Actual (after this PR)

Local test verification: pnpm test src/agents/cli-auth-epoch.test.ts → 12 passed (9 prior + 3 new). The 3 new cases prove (a) token rotation no longer flips, (b) OAuth and token epochs match, (c) auth-profile token rotation no longer flips. A real email identity change still flips the epoch (regression guard).

Evidence

RUN  v4.1.5
Test Files  1 passed (1)
     Tests  12 passed (12)

pnpm test src/agents/cli-auth-epoch.test.ts → 12 passed. pnpm test src/agents/cli-credentials.test.ts → 16 passed (no regressions). pnpm test src/agents/cli-session.test.ts → 12 passed (auth-epoch consumer). pnpm tsgo:core and pnpm tsgo:core:test clean. oxlint 0 warnings, 0 errors. Format check clean.

Followed clawsweeper acceptance criteria from the issue:

  • pnpm test src/agents/cli-auth-epoch.test.ts src/agents/cli-credentials.test.ts
  • pnpm exec oxfmt --check --threads=1 src/agents/cli-auth-epoch.ts src/agents/cli-auth-epoch.test.ts

Human Verification

  • Verified the new regression cases catch the bug: temporarily reverting either guard makes the relevant new test fail with the exact expect(second).toBe(first) mismatch.
  • Verified that real account changes still invalidate: the new "changes token auth-profile epochs when the email identity changes" test asserts the inverse and passes.
  • Verified that the OAuth side is unaffected: the existing OAuth tests (keeps identity-less claude cli oauth epochs stable across token changes, keeps oauth auth-profile epochs stable across token refreshes, changes oauth auth-profile epochs when the account identity changes) all still pass with their original assertions.
  • Confirmed no other consumers depend on the old token-flipping behavior — cli-session.test.ts (12 tests) and cli-credentials.test.ts (16 tests) both pass unchanged. prepareExecution at src/agents/cli-runner/prepare.ts:216 only reads the resolved string, not the encoding internals.
  • What I did not verify: live macOS keychain race reproduction. The fix is at the encoding layer, upstream of any keychain or parser behavior; with the encoding stable, the parser's fallthrough is harmless.

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.

Compatibility / Migration

  • Backward compatible? Yes for runtime behavior. The CLI_AUTH_EPOCH_VERSION bump (4 → 5) means cached epochs from prior versions invalidate once on first read; users see one expected auth-epoch reset on the upgrade boundary, then stable epochs afterward.
  • Config/env changes? No
  • Migration needed? No

Risks and Mitigations

  • Risk: A user actually using a static type: "token" credential (e.g. PAT) and switching to a different token for the SAME identity will no longer get a session reset on the swap. Mitigation: This is the intended behavior per Ayaan's OAuth pattern — token replacement is an authorized refresh, not an identity change. Real identity changes (different email/provider/tokenRef) still invalidate.
  • Risk: The parseClaudeCliOauthCredential partial-read fallthrough still produces a "token" credential type that gets persisted to auth-profile, even though the hash is now stable. The semantic is misleading (claude-cli OAuth users may see a "token" auth-profile entry). Mitigation: Out of scope for this PR per the reporter's preferred fix shape ("identity-only hash for both types"). If parser hardening is desired as defense-in-depth, that's a clean follow-up — the current fix is sufficient for the reported symptom.
  • Risk: CLI_AUTH_EPOCH_VERSION bump invalidates cached epochs once. Mitigation: Documented behavior; one-time upgrade-boundary reset is acceptable cost vs ongoing rotation-induced resets.

Changed files

  • CHANGELOG.md (modified, +1/-0)
  • src/agents/cli-auth-epoch.test.ts (modified, +127/-3)
  • src/agents/cli-auth-epoch.ts (modified, +19/-6)

Code Example

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

---

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

---

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

---

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

---

function encodeClaudeCredential(credential) {
    return encodeOAuthIdentity(credential);
}
RAW_BUFFERClick to expand / collapse

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.

extent analysis

TL;DR

Update the encodeClaudeCredential function to use an identity-only hash for both OAuth and token credentials to prevent session resets due to token rotations.

Guidance

  • Identify the encodeClaudeCredential function in dist/prepare.runtime-*.js and update it to use encodeOAuthIdentity for both credential types.
  • Verify that the updated function correctly hashes only stable identity fields, excluding rotating access tokens.
  • Apply the same change to encodeAuthProfileCredential in dist/prepare.runtime-*.js to ensure consistency.
  • After applying the fix, monitor the gateway logs for cli session reset: reason=auth-epoch messages to confirm that session resets have stopped.

Example

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

Notes

The suggested fix assumes that the issue is caused by the inclusion of rotating access tokens in the auth-epoch hash. If the issue persists after applying the fix, further investigation may be necessary to identify other potential causes.

Recommendation

Apply the suggested fix to update the encodeClaudeCredential function to use an identity-only hash, as this is the most robust solution to prevent session resets due to token rotations.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - ✅(Solved) Fix claude-cli auth-epoch flips on token rotation, forcing session resets mid-conversation [1 pull requests, 2 comments, 2 participants]