[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
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:
- One client refreshes the shared session, which rotates the IdP refresh token.
- 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". - 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.
- 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
- Run a Claude apps gateway with an OIDC IdP that issues rotating, single-use refresh tokens (here JumpCloud), with
offline_accessin the scopes andsession.ttl_hours: 1. - On one machine, sign in with the terminal CLI:
/loginto the gateway (managed settingsforceLoginMethod: "gateway"). - Open the Claude Code VS Code extension as the same OS user. It uses the stored sign-in.
- Use both clients, then leave both running past the 1-hour gateway token lifetime.
- Let one client refresh first, for example send a prompt in the terminal, or restart the terminal session.
- Then use the other client, or wait for its hourly managed-settings fetch.
- 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". - Run
/loginin 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_grantfailures.
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
- JumpCloud refresh tokens are single-use. Each
session.refreshexchanges the session's refresh token and rotates it. - 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.
- 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.
- 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
- Is this a known issue for Claude Code processes that share a gateway credential (CLI + VS Code extension)? Is a fix planned?
- 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.
- On
invalid_grant, can a client re-read the stored credential (another process may have refreshed or re-signed in) before clearing it? - Can the VS Code extension pick up a credential re-created by another process (e.g.
/loginin the terminal) without a restart? - Can the gateway include
sub/email(or a session ID) on failedsession.refreshlog events? Today failures cannot be attributed to a user. - Recommended gateway settings for IdPs with rotating refresh tokens. For example, would a longer
session.ttl_hoursbe 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.