[BUG] Claude apps gateway: CLI and VS Code sharing a session present a stale refresh token; IdP with rotating tokens revokes the chain and both clients are logged out

Status Open
Reported on v2.1.286
Maintainer reply None cached
Activity 0 comments · opened Oct 8, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

With a self-hosted Claude apps gateway and an IdP that issues single-use (rotating) refresh tokens (JumpCloud), Claude Code clients on the same machine that share one gateway sign-in (the terminal CLI and the VS Code extension) regularly lose their session:

  1. One client refreshes the shared session, which rotates the IdP refresh token.
  2. The other client later refreshes with the stale token it still holds in memory. The IdP rejects it: invalid_grant, "The refresh token was already used".
  3. After that reuse, every later refresh of that session fails with "The refresh token is malformed or not valid", including from the client that had refreshed successfully. The IdP appears to revoke the whole token chain on reuse.
  4. Both clients clear their credential ([gateway-refresh] IdP rejected refresh token; clearing it) and answer every request with "Cloud gateway session expired — run /login to reconnect".

The VS Code extension also stays signed out after /login succeeds in the terminal, until the extension restarts.

Claude Desktop, which has its own gateway session on the same machine, is not affected. MCP OAuth for a remote server using the same IdP (also with rotating refresh tokens) is not affected either.

Impact: daily, for users who keep both the terminal and the VS Code extension open. The gateway logged 20 session.refresh failures in 7 days, across a handful of pilot users. Reproduced twice on 2026-10-08 with client logs.

What Should Happen?

Claude Code clients that share a gateway sign-in coordinate refreshes, or re-read the stored credential immediately before refreshing, so that no client ever presents an already-used refresh token. The shared session then survives across the 1-hour gateway token lifetime, as Claude Desktop's session does.

After /login in one client, the other clients sharing the credential pick up the new sign-in without a restart.

Error Messages/Logs

All times UTC, 2026-10-08. User email redacted as `<user>`; same user throughout. The internal hostname is shortened to `claude-gateway.internal.<domain>`.

**Client error (CLI and VS Code):**


API error (attempt 1/11): Cloud gateway session expired — run /login to reconnect.


**CLI debug log** (`claude --debug`, 2.1.286):


2026-10-08T13:35:35.043Z [DEBUG] [gateway-refresh] transient failure: getaddrinfo ENOTFOUND claude-gateway.internal.<domain>
2026-10-08T13:35:39.335Z [DEBUG] Remote settings: Retry 4/5 after 4840ms (network_error)
2026-10-08T13:35:45.217Z [WARN] [gateway-refresh] IdP rejected refresh token; clearing it
2026-10-08T13:35:45.948Z [DEBUG] Remote settings: Fetch attempt failed (http_401)
2026-10-08T13:37:51.733Z [ERROR] API error (attempt 1/11): Cloud gateway session expired — run /login to reconnect.


**VS Code extension log** (Output → Claude Code, 2.1.292):


2026-10-08T13:37:41.722Z [WARN] [gateway-refresh] IdP rejected refresh token; clearing it
2026-10-08T13:37:41.727Z [ERROR] API error (attempt 1/11): Cloud gateway session expired — run /login to reconnect.
2026-10-08T13:39:42.804Z [WARN] [gateway-refresh] IdP rejected refresh token; clearing it
2026-10-08T13:39:43.529Z [DEBUG] Remote settings: Fetch attempt failed (http_401)


**Gateway log** (one failed refresh in full):


warn refresh failed request_id=08eda567-050a-452e-a83e-4380d16dae27: invalid_grant (The provided authorization grant (e.g., authorization code, resource owner credentials) or refresh token is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client. The refresh token was already used.) (at token_endpoint, answered invalid_grant)


**Annotated timeline, reproduction 1 (12:34–13:40).** The CLI was relaunched with `claude --debug` at about 12:34, using the stored sign-in from a `/login` at 10:24:53. The VS Code extension was running throughout.

| Time | Gateway `session.refresh` | Client log | Reading |
|---|---|---|---|
| 10:24:53.410 | `session.mint` success | Terminal `/login` | Shared CLI + VS Code session created |
| 11:39:52.806 | success | VS Code | VS Code refreshes the shared session |
| 12:35:07.593 | success | not in the excerpts; probably the relaunched CLI | Shared session refreshed; the token in VS Code's memory is now stale |
| 12:35:28.180 | success | not attributed | — |
| **12:36:01.194** | **fail**: *"already used"* (`b26bf7c2-ad50-423c-a104-16606853be8a`) | VS Code: `IdP rejected refresh token; clearing it`; its next request still succeeds (current JWT still valid) | **VS Code presents the stale token** |
| 12:39:42.645 | **fail**: *"malformed or not valid"* (`be5f1066-9e2d-4b12-a401-6d0e111b8190`) | VS Code: `IdP rejected refresh token; clearing it` | — |
| 13:35:35–39 | — | CLI: `[gateway-refresh] transient failure: getaddrinfo ENOTFOUND …` (retries) | Brief DNS/VPN unavailability |
| **13:35:45.076** | **fail**: *"malformed or not valid"* (`0ad2b3c3-6a30-4059-a3d0-17d6ea0c411c`) | CLI 13:35:45.217: `IdP rejected refresh token; clearing it` | **The CLI's token is also dead**, although the CLI refreshed successfully at 12:35 |
| 13:35:45.502 | success | — (neither Claude Code client) | Claude Desktop, separate session |
| 13:37:41.581 | **fail**: *"malformed or not valid"* | VS Code: `IdP rejected refresh token; clearing it`, then *"Cloud gateway session expired"* | — |
| 13:37:51.733 | — | CLI: *"Cloud gateway session expired — run /login to reconnect"* | — |
| 13:39:42.668 | **fail**: *"malformed or not valid"* | VS Code: `IdP rejected refresh token; clearing it` (hourly managed-settings fetch) | — |

**Reproduction 2, the same morning (gateway + VS Code log):**


08:00:54.942  session.refresh  success  (several clients waking; VS Code log: 08:00:55 "refreshed gateway JWT" after ENOTFOUND retries)
09:23:26.111  session.refresh  fail     invalid_grant  "The refresh token was already used."          request_id=08eda567-050a-452e-a83e-4380d16dae27
09:23:26.623  session.refresh  fail     invalid_grant  "The refresh token is malformed or not valid."  request_id=81d3f9a3-6c3e-4baf-919f-25394ba4d57f
09:23:27.537  session.refresh  success  (same user; not attributed)


VS Code: `09:23:26.958 [WARN] [gateway-refresh] IdP rejected refresh token; clearing it`. It then returned *"Cloud gateway session expired"* at 09:51 and **at 10:23:05, one second after a successful `/login` in the terminal** (gateway `session.refresh` success at 10:23:04, `session.mint` at 10:24:53). It worked again only after the extension restarted at 10:25. Same pattern: *already used* first, then *malformed or not valid*.

Steps to Reproduce

  1. Run a Claude apps gateway with an OIDC IdP that issues rotating, single-use refresh tokens (here JumpCloud), with offline_access in the scopes and session.ttl_hours: 1.
  2. On one machine, sign in with the terminal CLI: /login to the gateway (managed settings forceLoginMethod: "gateway").
  3. Open the Claude Code VS Code extension as the same OS user. It uses the stored sign-in.
  4. Use both clients, then leave both running past the 1-hour gateway token lifetime.
  5. Let one client refresh first, for example send a prompt in the terminal, or restart the terminal session.
  6. Then use the other client, or wait for its hourly managed-settings fetch.
  7. Result: the second client's refresh fails with invalid_grant "already used". From then on every refresh of that session fails with "malformed or not valid", and both clients report "Cloud gateway session expired — run /login to reconnect".
  8. Run /login in the terminal. The terminal works again; the VS Code extension still reports session expired until it restarts (Developer: Reload Window).

Control: with only one Claude Code client running (terminal closed, VS Code open), refreshes succeed. Claude Desktop with its own session never fails.

Claude Model

None

Is this a regression?

I don't know

Last Working Version

_No response_

Claude Code Version

2.1.286, 2.1.292

Platform

AWS Bedrock

Operating System

Windows

Terminal/Shell

PowerShell

Additional Information

Related: #58066 (same multi-process refresh race, on the claude.ai OAuth path, 2.1.138, closed as duplicate). This report is the Claude apps gateway refresh path ([gateway-refresh]) on 2.1.286/2.1.292, i.e. after the cross-process refresh lock fixes in 2.1.282/2.1.285. In our setup claude.ai-path and MCP OAuth refresh are not affected; only the gateway session refresh is.

Environment

| Item | Value |
|---|---|
| Gateway | Claude apps gateway, Claude Code 2.1.274 (claude gateway), ECS Fargate, 1 task, behind an internal ALB; Amazon Bedrock upstream |
| Gateway session config | session.ttl_hours: 1; OIDC scopes [openid, profile, email, offline_access]; token_endpoint_auth_method: client_secret_post; userinfo_fallback: true; scope_on_refresh unset |
| IdP | JumpCloud OIDC (https://oauth.id.jumpcloud.com/). Refresh tokens are single-use: each refresh returns a new refresh token and invalidates the old one. Refresh token lifetime 720 h |
| Clients (same Windows laptop, same OS user) | Claude Code VS Code extension 2.1.292 (cc_entrypoint=claude-vscode); Claude Code CLI in the terminal 2.1.286 (cc_entrypoint=cli); Claude Desktop (gateway mode, bootstrapUrl) |
| Client config | Machine managed settings forceLoginMethod: "gateway", forceLoginGatewayUrl, parentSettingsBehavior: "merge"; gateway policy delivered per user |
| Network | Gateway reachable only over the corporate Client VPN; clients reach it from VPN NAT addresses |

Seven-day counts (gateway): 20 session.refresh failures, all invalid_grant at token_endpoint: 13 "malformed or not valid", 7 "already used". They occur daily, clustered shortly after successful refreshes. Failed refreshes carry no sub/email, and all users share the VPN NAT addresses, so a failure can be tied to a user only by timing.

Control observations

  • Claude Desktop on the same laptop, with its own gateway session, refreshed silently overnight and throughout both reproductions, with no sign-in prompt.
  • Terminal closed, VS Code and Desktop only (11:39): two successful refreshes 0.8 s apart, Desktop (followed by desktop_bootstrap.serve) and VS Code, with no failure.
  • Remote MCP server with OAuth through the same IdP (rotating refresh tokens, offline_access), configured in both the terminal and VS Code: stayed connected overnight and through both reproductions, with no re-authentication. Claude Code's MCP OAuth refresh appears to handle multiple clients (cf. changelog 2.1.59, 2.1.136); the gateway session refresh does not.
  • DNS/VPN unavailability (on wake, or briefly before the refreshes at 09:23 and 13:35) is handled as a transient failure and retried. It precedes, but does not explain, the invalid_grant failures.

Changelog review (up to 2.1.294). No entry addresses gateway session refresh across processes sharing a credential. Related fixes are already in our client versions (2.1.286, 2.1.292), and the problem persists:

  • 2.1.282: requests failing with "another Claude Code process is refreshing it"; sessions started while another window was refreshing the sign-in not retrying the policy fetch;
  • 2.1.285: two sessions recovering a stale login refresh lock;
  • 2.1.281: "Not logged in" persisting after logging in from another Claude Code process.

A cross-process refresh lock therefore exists, but the gateway refresh path still presents a stale, in-memory refresh token.

Analysis

  1. JumpCloud refresh tokens are single-use. Each session.refresh exchanges the session's refresh token and rotates it.
  2. The terminal CLI and the VS Code extension share one stored gateway credential, but each process keeps its own in-memory copy. Each refreshes independently: shortly before its JWT expires, on a request, or on the hourly managed-settings fetch.
  3. When one process refreshes, the other process's copy is stale. When that process next refreshes, it presents the stale token. The IdP answers "already used", and from then on rejects the session's current token as well ("malformed or not valid"). This is consistent with reuse detection revoking the whole token chain.
  4. Each client clears its credential on the rejection. The VS Code extension does not re-read the credential after another process signs in again, so it stays signed out until restarted.

Not yet determined

  • Which client made the successful refreshes at 12:35:07, 12:35:28 and 09:23:27 (the relaunched CLI, VS Code or Desktop). Full client logs for those minutes are available on request.
  • Whether JumpCloud's chain revocation on reuse is documented behaviour. The observed pattern (already used, then only malformed or not valid for that session) is consistent across both reproductions.

Questions

  1. Is this a known issue for Claude Code processes that share a gateway credential (CLI + VS Code extension)? Is a fix planned?
  2. Can clients re-read the stored credential immediately before refreshing, and serialize refreshes across processes, so that a stale refresh token is never presented? With IdPs that revoke the token chain on reuse, a single stale refresh logs out every client sharing the session.
  3. On invalid_grant, can a client re-read the stored credential (another process may have refreshed or re-signed in) before clearing it?
  4. Can the VS Code extension pick up a credential re-created by another process (e.g. /login in the terminal) without a restart?
  5. Can the gateway include sub/email (or a session ID) on failed session.refresh log events? Today failures cannot be attributed to a user.
  6. Recommended gateway settings for IdPs with rotating refresh tokens. For example, would a longer session.ttl_hours be the advised mitigation, given the revocation trade-off?

Workaround in use: after "session expired", run /login in the terminal, then reload the VS Code window (Command Palette → Developer: Reload Window). Using one Claude Code client (terminal or VS Code) at a time avoids the problem.

View original on GitHub ↗