Skip to content

Fix Claude CLI session continuity after token refresh#70132

Merged
obviyus merged 4 commits into
mainfrom
fix/claude-cli-auth-epoch-continuity
Apr 22, 2026
Merged

Fix Claude CLI session continuity after token refresh#70132
obviyus merged 4 commits into
mainfrom
fix/claude-cli-auth-epoch-continuity

Conversation

@obviyus

@obviyus obviyus commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

Fixes Claude CLI sessions being discarded after a normal OAuth token refresh and gateway restart.

What changed:

  • hash stable OAuth identity material for CLI session auth epochs
  • version stored auth epochs so existing bindings upgrade without one-time session loss
  • add regression coverage for refresh-stable epochs and binding reuse

Verification:

  • pnpm check:changed

@obviyus
obviyus requested a review from a team as a code owner April 22, 2026 11:02
@aisle-research-bot

aisle-research-bot Bot commented Apr 22, 2026

Copy link
Copy Markdown

🔒 Aisle Security Analysis

We found 1 potential security issue(s) in this PR:

# Severity Title
1 🟠 High Auth epoch mismatch ignored for unversioned bindings enables stale session reuse after credential changes
1. 🟠 Auth epoch mismatch ignored for unversioned bindings enables stale session reuse after credential changes
Property Value
Severity High
CWE CWE-613
Location src/agents/cli-session.ts:151-157

Description

resolveCliSessionReuse only invalidates a stored session when the stored authEpoch differs and the stored authEpochVersion matches the current version. For older/legacy bindings where authEpochVersion is missing/undefined, auth epoch mismatches are ignored, so the CLI may reuse an existing session even when credentials/epoch have changed.

Impact:

  • After upgrading to CLI_AUTH_EPOCH_VERSION = 2, any previously persisted session binding without authEpochVersion can remain reusable even if the newly computed authEpoch changes due to credential rotation/logout.
  • This weakens session invalidation semantics and can keep a session authenticated longer than intended (stale session reuse).

Vulnerable code:

const storedAuthEpoch = normalizeOptionalString(binding?.authEpoch);
if (
  binding?.authEpochVersion === params.authEpochVersion &&
  storedAuthEpoch !== currentAuthEpoch
) {
  return { invalidatedReason: "auth-epoch" };
}

Recommendation

Fail closed for missing/unknown authEpochVersion (or explicitly treat it as incompatible), so any epoch mismatch invalidates the session. For example:

const storedAuthEpoch = normalizeOptionalString(binding?.authEpoch);
const storedVersion = typeof binding?.authEpochVersion === "number" ? binding.authEpochVersion : undefined;// Invalidate when versions differ OR are missing, unless you can safely recompute/translate the stored epoch.
if (storedVersion !== params.authEpochVersion) {
  return { invalidatedReason: "auth-epoch" };
}
if (storedAuthEpoch !== currentAuthEpoch) {
  return { invalidatedReason: "auth-epoch" };
}

If you need a migration path, consider:

  • invalidating once and writing a new binding with authEpochVersion, or
  • storing multiple epoch values (v1/v2) and comparing appropriately.

Avoid accepting unversioned epochs when authEpoch is intended to reflect current credential state.


Analyzed PR: #70132 at commit 10bc643

Last updated on: 2026-04-22T11:38:49Z

@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: S maintainer Maintainer-authored PR labels Apr 22, 2026
@greptile-apps

greptile-apps Bot commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes OAuth token refresh causing CLI session loss by hashing only stable identity material (refresh token, provider) instead of volatile fields (access token, expiry) into the auth epoch, and introducing a versioned epoch scheme (authEpochVersion: 2) that lets existing unversioned bindings pass through once during upgrade without dropping the session.

The implementation is well-structured and thoroughly tested. One mild concern: authEpochVersion is declared optional in resolveCliSessionReuse's parameter type, so a future caller omitting it would silently bypass epoch validation even for already-versioned bindings (version undefined vs 2 → check skipped). All current production callers always pass the constant, so no immediate breakage, but hardening the signature would prevent the footgun.

Confidence Score: 4/5

Safe to merge; the one concern is a latent footgun in the optional parameter signature, not a current breakage.

All production callers correctly pass authEpochVersion and the epoch stability fix is sound. The P2 finding about the optional authEpochVersion creating a symmetrical bypass for future callers is a code-quality concern rather than a present defect, but it's subtle enough to warrant a look before merging.

src/agents/cli-session.ts — the authEpochVersion optional parameter type in resolveCliSessionReuse

Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/agents/cli-session.ts
Line: 152-157

Comment:
**Version-mismatch bypass applies symmetrically in both directions**

The guard `binding?.authEpochVersion === params.authEpochVersion` skips epoch validation whenever the versions differ — including the case where `params.authEpochVersion` is omitted (`undefined`) but the stored binding already carries `authEpochVersion: 2`. In that scenario, a versioned binding would silently pass the epoch check even if the refresh token had changed. The production call-site in `prepare.ts` always supplies `authEpochVersion: CLI_AUTH_EPOCH_VERSION`, so this isn't broken today, but `authEpochVersion` being optional in the parameter type makes it easy for a future caller to omit it and unknowingly get epoch validation bypassed for already-versioned bindings.

Consider making `authEpochVersion` a required parameter (or defaulting it to `CLI_AUTH_EPOCH_VERSION` inside the function) so omissions are caught at compile time rather than silently degrading security.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "test(cli): cover oauth auth epoch contin..." | Re-trigger Greptile

Comment thread src/agents/cli-session.ts
@obviyus

obviyus commented Apr 22, 2026

Copy link
Copy Markdown
Contributor Author

Local smoke passed on this branch.

Covered:

  • rebuilt local dist and restarted the gateway service
  • confirmed Claude CLI warm-session reuse within one gateway process
  • confirmed Claude CLI resumes the same conversation after gateway restart
  • confirmed an unversioned/pre-fix auth-epoch binding upgrades to v2 without resetting the Claude session
  • checked logs for auth-epoch resets; none appeared during the smoke

Only unrelated noise seen: existing channel network warnings.

@obviyus obviyus self-assigned this Apr 22, 2026
@obviyus
obviyus force-pushed the fix/claude-cli-auth-epoch-continuity branch from 333b30d to 1943340 Compare April 22, 2026 11:28
@obviyus
obviyus merged commit 4a16cf8 into main Apr 22, 2026
83 checks passed
obviyus added a commit that referenced this pull request Apr 22, 2026
@obviyus
obviyus deleted the fix/claude-cli-auth-epoch-continuity branch April 22, 2026 11:33
@obviyus

obviyus commented Apr 22, 2026

Copy link
Copy Markdown
Contributor Author

Landed via rebase.

Commits:

Verified locally:

  • pnpm test src/agents/cli-auth-epoch.test.ts src/agents/cli-session.test.ts src/agents/cli-runner/prepare.test.ts src/agents/cli-runner.reliability.test.ts src/agents/cli-runner.spawn.test.ts
  • pnpm check:changed

openperf added a commit to openperf/moltbot that referenced this pull request Apr 25, 2026
getLocalCliCredentialFingerprint only switched on claude-cli and
codex-cli, so google-gemini-cli fell through to undefined and the
stored session's authEpoch never bound to the user's local Gemini
OAuth identity.

Read ~/.gemini/oauth_creds.json through the same cached-reader pattern
as Codex/MiniMax, and encode a stable identity fingerprint from the
refresh token (stable across Google's access-token rotation, flips on
re-authentication), matching openclaw#70132's stability contract.

Adds: readGeminiCliCredentials(Cached) in cli-credentials.ts, a
'google-gemini-cli' case in getLocalCliCredentialFingerprint, and
parallel test coverage alongside the existing claude/codex cases.
ogt-redknie pushed a commit to ogt-redknie/OPENX that referenced this pull request May 2, 2026
ogt-redknie pushed a commit to ogt-redknie/OPENX that referenced this pull request May 2, 2026
zhonghe0615 pushed a commit to zhonghe0615/openclaw that referenced this pull request May 7, 2026
zhonghe0615 pushed a commit to zhonghe0615/openclaw that referenced this pull request May 7, 2026
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
globalcaos pushed a commit to globalcaos/tinkerclaw that referenced this pull request May 13, 2026
globalcaos pushed a commit to globalcaos/tinkerclaw that referenced this pull request May 13, 2026
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 24, 2026
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 24, 2026
jameslcowan pushed a commit to jameslcowan/openclaw that referenced this pull request Jun 2, 2026
jameslcowan pushed a commit to jameslcowan/openclaw that referenced this pull request Jun 2, 2026
sablehead pushed a commit to sablehead/openclaw that referenced this pull request Jun 10, 2026
sablehead pushed a commit to sablehead/openclaw that referenced this pull request Jun 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling maintainer Maintainer-authored PR size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant