[BUG] Windows/MSIX: stealth update relaunch fails with 0x80070020 — app dead 9h, then re-seeds a dead v1 OAuth grant and forces re-login
All timestamps are local, PDT (UTC−07:00) — the desktop main.log is local time, not UTC. Related, not duplicates: #87950, #81512, #89812 (same terminal error, different trigger, all Linux/macOS), #91469 (same two config keys, different surface).
Preflight Checklist
- [x] I have searched existing issues
- [x] This is a single bug report (two tightly-coupled defects in one causal chain; happy to split)
- [x] I am using the latest version of Claude Code
Summary
An MSIX auto-update applied cleanly at 2026-09-03 23:11:10, but the stealth relaunch failed with 0x80070020 (ERROR_SHARING_VIOLATION) on both AppX container creation and LaunchProcess. The app did not come back. It stayed dead for 9h 44m while the machine was awake for the next four hours, leaving a Windows "already running" dialog on screen that I dismissed the following morning.
When I finally launched it by hand at 08:55:41, that start found the stale relaunch marker, seeded oauth-v2 from the legacy v1 token cache, and hit a refresh token that had already died on 2026-09-02. The failed refresh escalated into a scope expansion, which the server correctly rejected as 403 session_stale_relogin — forcing a full interactive re-login.
Two defects, one chain:
| | Defect | Severity |
|---|---|---|
| A | Stealth update relaunch fails with 0x80070020; no retry, no recovery, app simply never restarts | App unusable until manually launched |
| B | Post-failure cold start seeds oauth-v2 from a v1 grant whose refresh token already failed → forced re-login | Full interactive sign-in |
Defect A is the trigger. Defect B is why it costs a login rather than just a restart.
Environment
| | |
|---|---|
| App | Claude Desktop 1.46388.1.0, MSIX (WindowsApps\Claude_1.46388.1.0_x64__pzs8sxrjxfjjc) |
| Upgraded from | 1.44121.4.0 |
| Staged next | 1.46388.2.0 — already deferred, see "Armed to recur" |
| Claude Code | 2.1.260 |
| OS | Windows 11 Pro 10.0.26200, x64 |
| Electron/Node | Node 24.18.1 |
| Account | Personal Max 20x, Google SSO, installMethod: native |
| Config | %APPDATA%\Claude\config.json |
| Logs | %LOCALAPPDATA%\Claude\Logs\main.log; Microsoft-Windows-AppModel-Runtime/Admin; Microsoft-Windows-AppXDeploymentServer/Operational |
Defect A — the relaunch never happened
The app quit itself cleanly for the update:
2026-09-03 23:11:07 [info] [stealth-update] Triggering stealth update after idle timeout
2026-09-03 23:11:07 [info] [stealth-relaunch] Saved z-order anchor: 0x20e19ea (above our 0x7f0580)
2026-09-03 23:11:07 [info] [stealth-relaunch] Saved navigation history (36 entries, active=35)
2026-09-03 23:11:07 [info] [CCD] Killing 7 PTY process tree(s) on quit
2026-09-03 23:11:08 [info] beforeQuitForUpdate handler fired, going down for update
2026-09-03 23:11:08 [info] Windows session ending (close-app) - quitting the app
MSIX registration succeeded:
23:11:08 [603] Started deployment RegisterByPackageFamilyName ... ForceApplicationShutdownOption
23:11:08 [855] updateList: Claude_1.44121.4.0 is updating to Claude_1.46388.1.0
23:11:10 [400] Deployment Register operation ... finished successfully.
23:11:10 [472] Moving package folder ...Claude_1.44121.4.0... to ...\WindowsApps\Deleted\...
Then the relaunch died — Microsoft-Windows-AppModel-Runtime/Admin:
23:11:10 [215] Error 0x80070020: Cannot create the Desktop AppX container for package
Claude_1.46388.1.0_x64__pzs8sxrjxfjjc because an error was encountered converting the job.
23:11:10 [215] Error 0x80070020: (repeated)
23:11:10 [208] Error 0x80070020: Cannot create the process for package
Claude_1.46388.1.0_x64__pzs8sxrjxfjjc because an error was encountered
while configuring runtime. [LaunchProcess]
0x80070020 is ERROR_SHARING_VIOLATION. The old package's container was still alive — and stayed alive long past the update:
2026-09-04 00:54:47 [217] Destroyed Desktop AppX container {F68C6D20-...}
for package Claude_1.44121.4.0_x64__pzs8sxrjxfjjc
That is 1h 43m after the update registered. ForceApplicationShutdownOption was requested and evidently did not fully take.
There is no retry. main.log has zero entries between 23:11:08 and 08:55:42, and AppModel-Runtime records no further launch attempt until I started it by hand. Over the previous 45 days this 0x80070020 appears exactly once — 2026-09-03 23:11:10 — so this is a race, not a steady-state condition.
Defect B — the eventual start re-seeds a dead OAuth grant
config.jsonholds two caches:oauth:tokenCache(v1, 4 entries) andoauth:tokenCacheV2(v2).- The v2 cache loads 3 entries on every startup — 2026-09-02 15:42:32, 2026-09-02 23:53:38, 2026-09-04 08:55:47. It never gains the fourth.
- The absent fourth is the only grant carrying
user:sessions:claude_code— the scope Claude Code requires. It exists only in v1, under the pre-migration bare-clientId key. The v2 cache holds the same client/org underacct:<ACCOUNT>|<CLIENT_ID>:…but with the narrower setuser:inference user:file_upload user:profile. - The manual start recognized the abandoned relaunch and then filled the gap from v1:
````
08:55:43 [stealth-relaunch] Loaded z-order anchor: 0x20e19ea
08:55:43 [event-loop-stall] update relaunch (stale marker) stands in for an unheard suspend
08:55:47 [oauth-v2] loaded token cache with 3 entries
08:55:47 [oauth-v2] seeded from v1 cache for orgId=<ORG>
- That seeded refresh token was already dead — it first failed 2026-09-02 15:28:13 with
{"error":"invalid_grant","error_description":"Refresh token expired"}, and the v2 path never rewrites the v1 entry, so the dead grant persists inconfig.jsonindefinitely. - ```
08:55:49 [error] OAuth token refresh failed: status=400 ... invalid_grant
08:55:49 [oauth-v2] refresh rejected (auth_error); tombstoning and falling through
08:55:49 [oauth-v2] performing fresh /authorize
08:55:49 oauth failed: authorize returned 403 (permission_error, session_stale_relogin)
08:55:49 oauth authorize rejected with session_stale_relogin;
sessionKey is valid but too old for the requested scope expansion
08:55:49 [sessions-bridge] Cowork OAuth stale-session; parking bridge until re-login
```
- Full sign-in;
sessionKey rotatedat 09:00:09.
The 403 is a consequence, not the cause. The server is handed a dead grant and then asked for elevated scope by a session that is legitimately not fresh.
Timeline
| Time (PDT) | Source | Event |
|---|---|---|
| 2026-09-03 21:53:36 | main.log | [updater] Found an update, downloading |
| 2026-09-03 21:54:01 | main.log | Update downloaded and ready to install { releaseName: 'Claude 1.46388.1' } |
| 2026-09-03 23:11:07 | main.log | Idle timeout → [stealth-update] Triggering stealth update; z-order + 36 nav entries saved |
| 2026-09-03 23:11:08 | main.log | beforeQuitForUpdate → app quits |
| 2026-09-03 23:11:10 | AppXDeployment | MSIX Register succeeds; old package folder moved to Deleted |
| 2026-09-03 23:11:10 | AppModel-Runtime | 0x80070020 ×3 — container + LaunchProcess fail. App is now dead. |
| 2026-09-04 00:54:47 | AppModel-Runtime | Old 1.44121.4.0 AppX container finally destroyed (1h 43m late) |
| 2026-09-04 03:11:11 | Kernel-Power | System enters sleep (nothing to relaunch) |
| 2026-09-04 07:09:51 | Power-Troubleshooter | Wake, source: Power Button. Stale "already running" dialog dismissed |
| 2026-09-04 08:55:41 | AppModel-Runtime | Manual launch: Created process 75404 for Claude_pzs8sxrjxfjjc!Claude |
| 2026-09-04 08:55:43 | main.log | update relaunch (stale marker) stands in for an unheard suspend |
| 2026-09-04 08:55:47 | main.log | [oauth-v2] seeded from v1 cache |
| 2026-09-04 08:55:49 | main.log | invalid_grant → 403 session_stale_relogin |
| 2026-09-04 09:00:09 | main.log | sessionKey rotated — forced re-login |
App unavailable: 9h 44m (23:11:10 → 08:55:41). Time from manual launch to completed re-login: 4m 29s.
The previous forced re-login was 2026-09-02 15:47:17, giving a 41h 27m interval — which is what made this look like a ~48h session TTL. It is not. The CLI's own refresh token is valid 28 days (issued 2026-09-04 09:01:26, expires 2026-10-02 03:37).
Armed to recur
1.46388.2 downloaded at 08:56:46 and is already blocked behind the same condition:
08:56:44 [638] Packages were not updated because affected apps are still running.
Running apps: {Claude_pzs8sxrjxfjjc!Claude}
08:56:44 [658] Warning Marking package {Claude_1.46388.2.0} for deferred registration
because {Claude_1.46388.1.0} is still running.
The next idle-timeout stealth update will run the identical sequence against a live process tree.
What should happen
- Retry the relaunch. A
0x80070020onLaunchProcessafter a successful package Register should back off and retry, not abandon the app. Nine hours of downtime from a single transient sharing violation is the core harm. - Verify old-process teardown before registering.
ForceApplicationShutdownOptionreturned success while the old AppX container survived another 1h 43m. Wait on actual container destruction, or register with deferred registration and relaunch on next user activation. - Surface the failure. The user-visible artifact was a generic Windows "already running" dialog with no path to recovery. If the relaunch fails, the app should say so and offer a retry.
- Don't seed
oauth-v2from a v1 entry whose refresh token already failed. Tombstone the v1 entry on firstinvalid_grant, or validate before seeding. - Don't let a failed silent refresh escalate to a scope expansion. Re-requesting the scopes the session already holds would avoid the freshness check and recover without user interaction.
- Fix the v1→v2 migration. The v2 cache being stuck at 3 of 4 entries across every startup is the underlying defect; anyone with a
user:sessions:claude_codegrant is exposed.
Workaround
For defect B only, deleting the two keys clears the poisoned entry — per @VanDmitry's #91469, same artifact, different symptom (Claude in Chrome 401 loop):
# Fully quit Claude Desktop first.
$cfg = "$env:APPDATA\Claude\config.json"
Copy-Item $cfg "$cfg.bak-$(Get-Date -Format yyyyMMddTHHmmss)"
# Remove ONLY: oauth:tokenCache and oauth:tokenCacheV2
Costs one immediate re-login. No workaround for defect A short of disabling auto-update.
Relationship to existing reports
#87950, #81512 and #89812 report session_stale_relogin forced logouts every 24–48h and each explicitly rules out local credential storage, concluding the invalidation is server-side. On their evidence that is reasonable. This report is a different trigger reaching the same server error: those reporters state the app was never closed, whereas here the app was closed — by an update whose relaunch failed. The distinguishing log line is [oauth-v2] seeded from v1 cache, which returns zero results across the tracker.
Worth checking whether their logs show invalid_grant immediately preceding the 403. If so, the v1-seed path may be common to all four and the "server-side TTL" reading is wrong.
Not affected
For triage completeness, everything else survived cleanly: Claude Code CLI credentials, Gmail and Google Calendar connectors, the remote-control device bridge, and the Cowork sessions bridge (parked 08:55:49, restarted 09:00:09). No connector authorization was lost. The only unrelated failure in this log is plugin:pdf-viewer:pdf failing to spawn under npx with CONNECTION_CLOSED.