[BUG] Windows/MSIX: `git fsmonitor--daemon` spawned by Claude Code inherits the AppX container job, survives the update's forced shutdown, and blocks relaunch of the new version (0x80070020). Root cause + no-reboot workaround
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?
After the Claude Desktop auto-updater stages a new MSIX version and force-relaunches the app, every subsequent launch fails with the dialog "Another program is currently using this file" (HRESULT 0x80070020, AppModel-Runtime events 215/208). This symptom is already reported in #53247, #73107, #87879, #89680, #89687, #89912, #90269, #91482, #91736 and #91749, with the cause variously attributed to a race, cowork-svc.exe, an orphaned silo, or an unidentified leaked job object.
#73107 identified the correct mechanism (an orphaned Claude Code child process pinning the old version's container) with an elevated culprit (llama-svc.exe -elevated). This report shows the same mechanism with a far more common, entirely non-elevated culprit that will hit any Windows developer using git, and gives a no-reboot, no-elevation workaround. Reproduced three times in 24 hours across three consecutive updates (1.40609.1 → 1.44121.1 → 1.44121.2 → 1.44121.4).
The old version's Desktop AppX container is kept alive by a detached git fsmonitor--daemon process that a Claude Code session (Desktop Code tab) spawned indirectly. Git's file-system monitor daemon (core.fsmonitor=true) is started with --detach, inherits the container job, has no package identity, and is not terminated by the updater's ForceApplicationShutdown. While that container persists, Windows refuses to create a container for the new package version ("error encountered converting the job").
Proof (occurrence 2 of 3)
Pairing AppModel-Runtime event 210 (container created) with 217 (container destroyed) per container GUID showed the container for the PREVIOUS version, Claude_1.44121.1.0_x64__pzs8sxrjxfjjc {36DD7527-…}, created 2026-09-02 14:54:37 at normal app launch, was never destroyed. Its only surviving member:
PID 32504 git.exe "git fsmonitor--daemon run --detach --ipc-threads=8"
created 2026-09-02 15:14:09, parent PID dead, IsProcessInJob = true, no package identity
named pipe \\.\pipe\C_\<repo path>\.git\fsmonitor--daemon.ipc
Terminating PID 32504 produced event 217 for {36DD7527-…} the same second, and the next launch of 1.44121.2.0 succeeded (event 201, process created). No reboot, no elevation.
Occurrence 3, same day: update 1.44121.2.0 → 1.44121.4.0 staged 09:17, registered 09:50, relaunch failed identically. Surviving member of the 1.44121.2.0 container was PID 35836, git fsmonitor--daemon for a different repository, created 09:21 by a Claude Code session inside the desktop app. Killing it destroyed the container and 1.44121.4.0 launched.
Stopping the packaged CoworkVMService made no difference (tested), so the service is not the cause here. The service-log warning failed to configure recovery actions ... open service: Access is denied (#89711 / #91195) is present but unrelated to this failure.
Ruled out
- No process holds the package identity (
tasklist /apps), no file handle held on the package folder - Package folder intact, manifest and signature valid,
Get-AppxPackageStatus = Ok - CodeIntegrity 3010 catalog warnings occur for every version including working ones (benign)
- No Defender / AppLocker / Code Integrity blocks
What Should Happen?
The new version should launch after an auto-update without a reboot. Concretely:
- Child processes spawned by Claude Code (Code tab / Cowork / MCP servers / terminals) inside the packaged app should not inherit the Desktop AppX container job. Break them out (
CREATE_BREAKAWAY_FROM_JOB, or launch via an intermediate process outside the container) so detached daemons cannot keep the old container alive.git fsmonitor--daemonis the case found here; any--detach/daemonising child (file watchers, language servers, background node processes) will behave the same way. - Before the updater's forced shutdown/registration, enumerate the app's own container job and terminate survivors, or at least fail loudly naming them.
- On relaunch failure with 0x80070020, stop looping on
RepairAppRegistration(it cannot help, the package is fine) and surface the real error instead of the generic "Another program is currently using this file".
Error Messages/Logs
Microsoft-Windows-AppModel-Runtime/Admin (relaunch after update):
Event 210 Created Desktop AppX container {...} for package Claude_1.44121.2.0_x64__pzs8sxrjxfjjc
Event 211 Added process 39100 to Desktop AppX container ... (PID 39100 = cowork-svc.exe, restarted by the registration)
Event 215 0x80070020: Cannot create the Desktop AppX container for package Claude_1.44121.2.0_x64__pzs8sxrjxfjjc because an error was encountered converting the job.
Event 208 0x80070020: Cannot create the process for package Claude_1.44121.2.0_x64__pzs8sxrjxfjjc because an error was encountered while configuring runtime. [LaunchProcess]
Microsoft-Windows-AppXDeploymentServer/Operational (every manual launch from Start):
Event 603 Started deployment RegisterByPackageFullName ... Options ForceTargetApplicationShutdownOption,RepairAppRegistrationOption
Event 649 Trying to repair ACLs for \\?\C:\Program Files\WindowsApps\Claude_1.44121.2.0_x64__pzs8sxrjxfjjc
Event 9650 Successfully terminated service ... CoworkVMService
Event 400 Register ... finished successfully
Update sequence (Microsoft-Windows-AppXDeploymentServer/Operational, local time):
2026-09-02 23:54 Add of Claude_1.44121.2.0 (DeferRegistrationWhenPackagesAreInUse) succeeded
2026-09-03 00:04 Register of 1.44121.2.0 failed 0x80073D02 ("apps need to be closed: Claude_1.44121.1.0")
2026-09-03 00:05 RegisterByPackageFamilyName with ForceApplicationShutdown succeeded (3 s)
2026-09-03 00:05 Relaunch failed: AppModel-Runtime 215 / 208 as above
2026-09-03 08:22-08:25 Four manual launches, each RegisterByPackageFullName + RepairAppRegistration "success", launch fails identically
2026-09-03 09:16 Killed PID 32504 (git fsmonitor--daemon) -> AppModel-Runtime 217 container {36DD7527-...} destroyed same second -> next launch event 201 Created process, app runs
Surviving process:
PID 32504 git.exe "git fsmonitor--daemon run --detach --ipc-threads=8"
created 2026-09-02 15:14:09, parent PID dead, IsProcessInJob = true, no package identity
named pipe \\.\pipe\C_\<repo path>\.git\fsmonitor--daemon.ipc
C:\ProgramData\Claude\Logs\cowork-service.log (every service start, unrelated to this failure):
Warning: failed to configure recovery actions (a crashed service will stay down until reboot): open service: Access is denied.
Steps to Reproduce
- Windows 11, Claude Desktop installed as MSIX (self-updating), standard non-admin user.
- Have a git repository with the file-system monitor enabled:
git config core.fsmonitor true(Git for Windows 2.55.0 here). - Open the Desktop app's Code tab in that repository and let Claude Code run any
gitcommand (e.g.git status). Git spawnsgit fsmonitor--daemon run --detach --ipc-threads=8. Confirm it is inside the app's container job: the process has a dead parent,IsProcessInJob= true, and no package identity (tasklist /appsdoes not list it). - Wait for (or trigger) a Claude Desktop auto-update. The updater stages the new package, force-shuts the app (
ForceApplicationShutdown), registers the new version successfully, then relaunches. - Relaunch fails with "Another program is currently using this file". Event Viewer > Microsoft-Windows-AppModel-Runtime/Admin shows events 215 and 208 with 0x80070020 "error encountered converting the job". Every manual launch from Start repeats a RegisterByPackageFullName + RepairAppRegistration that reports success and then fails identically.
- Pair AppModel-Runtime events 210 and 217 by container GUID: the previous version's container has a 210 but no 217. The
git fsmonitor--daemonprocess from step 3 is still running and is the only member of that job. - End that git.exe process (Task Manager, or
Get-CimInstance Win32_Process -Filter "Name='git.exe'" | Where-Object CommandLine -like '*fsmonitor--daemon*' | ForEach-Object { Stop-Process -Id $_.ProcessId }). Event 217 fires for the old container immediately. - Launch Claude Desktop: event 201 (process created), the new version runs. No reboot, no elevation.
Reproduced three times in 24 hours (1.40609.1 → 1.44121.1 on 2026-09-02; 1.44121.1 → 1.44121.2 and 1.44121.2 → 1.44121.4 on 2026-09-03). The first two times were 'resolved' by a full reboot before the cause was known; the last two were resolved by killing the daemon.
Claude Model
Not sure / Multiple models
Is this a regression?
Yes, this worked in a previous version
Last Working Version
1.40609.1.0 (relaunch after the 2026-09-01 update worked; likely because no fsmonitor daemon had been spawned during that session, not because of a code difference)
Claude Code Version
2.1.259 (Claude Code) — bundled with Claude Desktop 1.44121.4.0 (Windows MSIX, package family Claude_pzs8sxrjxfjjc). Affected desktop versions: 1.44121.1.0, 1.44121.2.0, 1.44121.4.0
Platform
Anthropic API
Operating System
Windows
Terminal/Shell
Other
Additional Information
Environment
- Windows 11 Enterprise 24H2, build 26100.9168, domain-joined, standard (non-admin) user
- Claude Desktop MSIX, SignatureKind = Developer, self-updating; packaged service CoworkVMService (cowork-svc.exe, LocalSystem) present
- Git for Windows 2.55.0.windows.3,
core.fsmonitor=trueset per-repository - Microsoft Defender for Endpoint, no detections or blocks logged
How affected users can detect and fix it without a reboot
Pair events 210/217 in Microsoft-Windows-AppModel-Runtime/Admin to find a container GUID with no 217, then look for processes with a dead parent that are in a job but have no package identity. In all cases here it was exactly one git.exe with the fsmonitor--daemon command line. Ending that process (it is the user's own, no admin needed) fixes the launch immediately. Alternatively set core.fsmonitor=false in repositories opened from the desktop app.
Relationship to existing issues
- #73107 (open since July,
has repro): same mechanism, an orphaned Claude Code child pinning the old container, but the culprit there was an elevatedllama-svc.execonsole. Here it is an unelevatedgit fsmonitor--daemon, which any Windows git user withcore.fsmonitorenabled will spawn on the firstgit status. That makes the failure far more widespread than #73107 alone suggests, and explains why it recurs on every update for some users. - #89680 (orphaned processes holding the old container) describes the same mechanism without identifying the process.
- #53247, #87879, #89687, #89912, #90269, #91482, #91736, #91749 report the same 0x80070020 symptom. Several have been closed or labelled
invalid/duplicate, and #87879 ruled out surviving container members entirely, which suggests the root cause has been missed during triage.
The key fix is the one not yet proposed in any of those: spawn Claude Code's child processes with CREATE_BREAKAWAY_FROM_JOB (or via a launcher outside the container) so nothing a session starts can outlive the container. Reaping survivors at update time (#73107's suggestion) is a good second line of defence.
Happy to supply the full exported event logs (evtx) or run diagnostics on request.
20 Comments
Independent confirmation of this mechanism on another machine, with a different culprit process — which I think widens the fix requirement — plus one diagnostic detail I haven't seen in any of the linked issues.
1. Same mechanism, culprit is not git
Windows 11 Enterprise 26100, Claude Desktop MSIX (
Claude_pzs8sxrjxfjjc, SignatureKind Developer), stealth update1.40609.1.0 → 1.44121.2.0.Sequence matched this report exactly:
[stealth-update] Triggering stealth update after idle timeout→beforeQuitForUpdate handler fired, going down for update,main.logends there, never relaunches; every launch then failed with AppModel-Runtime 215/2080x80070020"error was encountered converting the job".Pairing 210/217 by container GUID (your method) found exactly one leaked container:
Its surviving member was not git — there was no
git fsmonitor--daemonprocess at all. It was a detached Node process that a Claude Code session inside the desktop app had spawned:Killing it produced the 217 for
{F6EB323D-...}immediately, and0x80070020stopped occurring:So the trigger generalises beyond
core.fsmonitor: any long-lived/detached child a Claude Code session starts inside the packaged app can pin the old container. That supports theCREATE_BREAKAWAY_FROM_JOBfix in this issue over anything git-specific.2. Why triage keeps "ruling out" surviving members
This is worth calling out explicitly, because it is a trap. The natural check — enumerate processes and ask for package identity (
GetPackageFamilyName/tasklist /apps) — reports nothing, because the surviving member is inside the container job but has no package identity. I initially concluded "no orphaned processes exist" for exactly this reason and went looking elsewhere for hours.The check that works is the negative one:
IsProcessInJob == trueAND no package identity AND dead parent, then intersect with processes created after the leaked container's 210 and before the app quit. On this machine that narrowed 488 in-job/no-identity processes down to a single candidate.3. New detail: with an elevated app, Appinfo holds the leaked container job
The app here is launched via "Run as administrator" (packaged full-trust apps can run elevated). In that configuration the leaked container job handle is held by the UAC elevation broker, not by PSM:
So
handle64.exe -a -p Appinfoand grepping\Container_Claude_*is a fast one-line detector for the elevated case, and the container name carries the old package version, which makes the orphan obvious when compared against(Get-AppxPackage -Name Claude).Version.Two caveats for anyone trying to clear it without finding the culprit process:
handle64 -c <h> -p <pid> -yfails with "The handle is invalid", andAppinfocannot be recycled — bothRestart-Service Appinfo -Forceandsc stop Appinfosilently fail (PID unchanged, SCM still reports RUNNING) despite advertisingSTOPPABLE/ zero dependents. Killing the culprit process is the only thing that released it.4. A second failure that survives clearing the container
Flagging this because it may be a distinct bug hiding behind the same dialog. After the container was verifiably released (no
\Container_Claude_*job, zero 215/208), the app still would not start:Activation of app Claude_pzs8sxrjxfjjc!Claude attempted. Execution state: ... The operation completed successfully.Claude.exeprocess, no event 201,main.logstill frozen at thebeforeQuitForUpdatelinecowork-svc.exeInvoke-CommandInDesktopPackage -PackageFamilyName Claude_pzs8sxrjxfjjc -AppId Claudehangs >5 min and launches nothingThe dialog also changed to
The service cannot accept control messages at this time(ERROR_SERVICE_CANNOT_ACCEPT_CTRL, 1061 / 0x80070425), and the reason is visible in SCM: every activation re-services the packaged service —so the activation races its own re-registration of
CoworkVMService. This lines up with #89925 (service installs/disables itself 7x during one update). Point 3 of this issue — stop loopingRepairAppRegistrationand surface the real error — would help here too.Things that report success but change nothing in this state:
IPackageDebugSettings::TerminateAllProcessesand::StopServicing(both return S_OK;GetPackageExecutionStatestays 0/UNKNOWN),Add-AppxPackage -Register ...\AppxManifest.xml -DisableDevelopmentMode, andsc stop CoworkVMServicebefore launching. Package health was never in question throughout:Status: Ok,IsPartiallyStaged: False,Get-AppxPackage -AllUsersshows the user asInstalled,Get-StartAppslists the AUMID. Only a reboot recovered it.5. Prevention that works today
For anyone blocked on this, the managed policy removes the trigger entirely, since the stealth relaunch only arms once an update is staged:
Booleans must be
REG_DWORD1/0. Read at launch only. Cost is manual updates — running Claude Setup still works and ignores the policy. NoteautoUpdaterEnforcementHoursalone is not sufficient: blank uses the 72 h default and then waits for 10+ minutes of idle, which is precisely the path that wedges.Happy to supply exported evtx or run further diagnostics.
the breakaway fix (fix 1) closes the launch failure but trades a loud 0x80070020 for a silent daemon leak. container membership was the one thing surfacing these orphans, it's what let you pair 210/217 and find the survivor. spawn the child with CREATE_BREAKAWAY_FROM_JOB and the app launches clean every update, but now each one strands another detached fsmonitor holding a repo's .git lock, and nothing enumerates them, including the reaper in fix 2, which walks the same container job you just broke out of. the failed launch was the only reaper you had.
Hitting this too. Windows 11, dialog referencing
Claude_1.46388.2.0_...underC:\Program Files\WindowsApps\. Happened twice so far, each time only a full restart recovered it — matches the mechanism described above.Confirmed the same failure with Android's ADB server, providing another case beyond Git fsmonitor.
Claude Desktop would not launch (
0x80070020, AppModel-Runtime events 215/208). Pairing container-created and container-destroyed events showed that a container for the previous Claude version was still alive.To identify its surviving member precisely:
Container_Claude_...job handle in theAppinfoservice's process.QueryInformationJobObjectwithJobObjectBasicProcessIdList.adb.exe.This checks membership in the specific stale container;
IsProcessInJob(process, NULL)alone only tells you that a process belongs to some job.After
adb kill-server, Windows immediately emitted event 217 for that old container. The current Claude Desktop then opened with a responsive window, whileCoworkVMServicewas Running / Automatic. Stopping Cowork and re-registering the package had not resolved the failure earlier. No reboot or app-data reset was needed.We then ran
adb start-serverfrom outside Claude Desktop, restoring the ADB server while Claude remained responsive. Stopping ADB interrupts active Android debugging connections, so this recovery is appropriate only after identifying it as the blocker.This confirms that the surviving-container mechanism also affects ADB. We did not capture the original command that spawned the server, so we cannot establish the exact spawn chain or offer a freshly reproduced update sequence. The directly verified result is that ADB was the sole remaining job member and stopping it released the old container and restored launch.
Same mechanism as this issue, fourth daemon family: Git for Windows' GnuPG
agents, spawned by GPG-signed commits made from the Code tab. The culprits
reported so far are all daemons a Claude Code session happened to start, and
gpg-agentbelongs on that list. It reaches anyone withcommit.gpgsign=true,which is a wider population than
core.fsmonitor=true.Environment
| | |
|---|---|
| OS | Windows 11 Pro 25H2, build 10.0.26200.9278 |
| Claude Desktop |
Claude_1.46388.4.0_x64__pzs8sxrjxfjjc|| Install source | sideloaded MSIX from claude.ai/download (
SignatureKind: Developer), not the Store || Update path |
1.46388.1.0to1.46388.4.0, staged while running, registered on the app's own idle-timeout quit || Git | 2.55.0.windows.2 |
| GnuPG | 2.4.9, bundled with Git for Windows |
| Signing |
commit.gpgsign=true,gpg.format=openpgp|| PowerShell | 7.6.5 |
The error
Microsoft-Windows-AppModel-Runtime/Admin, once per launch attempt, 33 timesbetween 09:09 and 10:21:
0x80070020isERROR_SHARING_VIOLATION, and "converting the job" is the jobobject being converted into a server silo.
The leaked container
The old version's container has a Created event and no Destroyed event:
Every other Claude container in the log pairs 210 with 217. That one never did.
What the app's own log shows
Everything above is Windows reporting on Claude. This is Claude reporting on
itself, from
%LOCALAPPDATA%\Claude\Logs\main.log, and it is the half that needsno Windows internals to act on:
The log then stops. The next entry is at 10:50, after I recovered it by hand.
Worth drawing out: the app carefully saved everything it needed to come back,
z-order anchor, navigation history, popout state, went down, and never returned.
Nothing in the log says the relaunch failed, because nothing appears to check. A
user sees the window vanish on idle and not come back, then gets a Windows shell
dialog naming no process if they try to start it again.
What was inside it
Sweeping for processes created during that container's lifetime whose parent had
since exited returned exactly three, all GnuPG daemons from Git for Windows,
started by a signed commit at 07:53:
They detach from their parent but cannot leave the job, exactly as described here
for
fsmonitor--daemon. Nothing else on the machine matched.Not
CoworkVMService, which is the obvious suspect and is the wrong one:stopping it changes nothing, and it gets a container of its own that succeeds
while the app's fails.
Recovery, confirmed
Ending those three produced the missing Destroyed event within seconds, and the
app launched clean:
No reboot, no sign-out, no elevation. One detail specific to GnuPG:
End
gpg-agentbeforekeyboxdandscdaemon. While it is alive itrespawns its helpers, and the replacements join the same job you are emptying.
Then relaunch Claude Desktop. The only other cost is a pinentry passphrase
prompt on the next signed commit, since the daemons respawn on demand.
Why it does not leak on every update
Worth adding, because "it happens on every update" and "it is a random race" are
both wrong. It leaks only when the app has left a process behind, so an update
landing on an idle or lightly used app is clean. The
04/09update on thismachine swapped versions with every container properly destroyed:
That session lasted about 12 minutes and spawned nothing that outlived it. The
07/09session ran nearly two hours and made a signed commit partway through.That also explains the spread of culprits in this thread.
fsmonitor--daemon,MCP servers, stray
findandhead, and now the GnuPG agents are all things aworking session leaves behind, not things an idle app creates. The common
factor is not which tool it is, it is that something outlived the app inside its
job.
Finding it on your own machine (for fellow users)
Read-only, no elevation. It pairs the container events by GUID, keeps the ones
created for a version that is no longer installed, then lists the orphaned
processes born inside that container's lifetime. Paste it into PowerShell:
LEAKED {GUID}followed by a process list means you can recover withoutrestarting: end those processes, supervisors before their helpers, and relaunch.
No leaked container.means something else is wrong and this is not your bug.Two caveats. The sweep lists every orphan born after the container, so on a
long-uptime machine it can name processes that were never in the job. Treat the
list as candidates and prefer daemons a Claude Code session would have started.
And the output prints full command lines, which include your own user profile
path, so redact before pasting it anywhere public.
Both the snippet above and the fuller script below need a PowerShell prompt,
Windows PowerShell 5.1 or PowerShell 7. Git Bash and cmd cannot run a
.ps1directly; from those, call
pwsh -NoProfile -File .\find-leaked-appx-container.ps1.If you want a fuller version, this one recognises known daemon families, prints
the exact
Stop-Processline already ordered correctly, and comes with thefixture tests that pin it:
https://gist.github.com/mustalk/3fbc27233444ac11d44fc04508c50cd6
A suggestion, if anyone from the team picks this up
The real fix is at quit time: end the container's job before going down for the
update, or spawn children so they never join it in the first place. This is only
about machines already in the state.
For those, the app is better placed to recover than any of us. It knows its own
package family, so it can pair the 210 and 217 events itself, find the container
that was never destroyed, and name what is still inside it. Right now Windows
shows "Another program is currently using this file", which names nothing and
sends everyone to a reboot.
On whether it should then end that process: offer, do not act, and only offer
for processes it can prove it spawned. It already tracks those, since it kills
its own child trees on quit. Provenance is knowable; criticality is not. The app
cannot tell whether someone's
gpg-agentis busy for something else, but it cantell whether it started it. Something like:
Naming the process and stating the side effect makes the choice an informed one.
Anything the app did not spawn should be named but not offered, the same way the
script above refuses to build a command for processes it does not recognise.
The bar to clear is low: the remedy people are using today is a reboot, which
ends every process on the machine.
the full restart works because it reaps every orphaned daemon at once, which is exactly why it hides the leak: you never see that each update stranded another fsmonitor, the reboot just clears them all silently and reports success. killing only the fsmonitor--daemon for that repo (dead parent, in-job, no package identity) drops the old container the same second, no reboot, and it actually shows you it was one survivor per update.
the adb case sharpens the breakaway tradeoff: once daemons are detached, an update-time reaper can't tell a stale fsmonitor survivor from a live adb.exe mid debug session. it either strands them like the leaked daemon or blind-kills real work. container membership was the one signal separating the two, and breakaway removes it before the reaper ever runs.
the 'offer only what it can prove it spawned' gate loses exactly the leaked set. provenance lives in-memory in the instance that went down, so the fresh version that comes up has no record it started those daemons, and can't distinguish them from the orphans your own caveat says to treat as candidates. gpg-agent makes it worse: it's a per-homedir singleton a plain shell commit could've spawned before the app ever ran. the process it most wants to offer is the one whose provenance died with the container.
Confirming the prediction: npx-spawned MCP servers are exactly this class
Your report calls the general case:
Confirmed on a different machine with a different and much less avoidable culprit — the MCP servers a Claude Code session spawns, plus the bundled Claude Code CLI itself. Same signature you measured for PID 32504:
| process |
IsProcessInJob| package identity ||---|---|---|
|
cmd.exe→npx(MCP launcher shim) | True | NONE ||
node.exe(@modelcontextprotocol/*,@azure-devops/mcp) | True | NONE ||
claude.exe(bundled CLI,%APPDATA%\Claude\claude-code\<ver>\) | True | NONE ||
Claude.exe(Electron, packaged) | True |Claude_1.49585.0.0_x64__pzs8sxrjxfjjc|31 processes in the container job with no package identity, against 13 with it. Environment: Claude Desktop
1.46388.4.0→1.49585.0.0, Claude Code 2.1.260, Windows 11 Enterprise 10.0.26200, MSIXSignatureKind: Developer, 3 concurrent sessions with ~5 MCP servers each.This widens the blast radius argument in your report.
core.fsmonitoris opt-in, but every MCP server configured in Claude Desktop spawns throughcmd.exe→npx→node.exeon every session, unconditionally, with no user setting involved. Any user with MCP servers configured is generating survivors continuously, which fits the reports where this recurs on literally every update.One nuance on the failure mode
This machine never reaches
0x80070020— no AppModel-Runtime 215/208 events in 21 days, only 201/211. It fails one step earlier, at register time with0x80073D02("apps need to be closed"), which is the variant in #89599. So the same orphan class yields either error depending on whether the force-register completed before the relaunch attempt. That may be worth noting in triage, because the two error codes are currently being filed as separate problems while sharing this root cause.On the proposed fix
CREATE_BREAKAWAY_FROM_JOBlooks right, with one implementation note: MCP servers are launched three levels deep (cmd.exe→npx→node.exe), and thecmd.exeshim is itself one of the survivors here. Breakaway has to be applied at the outermost spawn, otherwise the intermediate shim retains the job membership that the eventual daemon inherits. The same applies to the bundled CLI, which the desktop app spawns directly.Detection snippet
Pairs job membership with package identity in one pass, for other reporters trying to identify their own survivor:
Happy to run further diagnostics on this machine if it helps — it reproduces daily.
ForceApplicationShutdown reporting 'succeeded (3s)' is the same swallow one layer up: it only terminates package-identity processes, and your table's whole point is that these survivors have none. so the force-register returns success, the container it was meant to drain still holds those 31 orphans, and the next launch eats 0x80073D02. the step that's supposed to guarantee a clean slate structurally can't see what's keeping it dirty.
Windows desktop update leaves three Git monitors holding the old AppX container; launch fails with 0x80070020 until those monitors are stopped
Problem identification, diagnosis, and verification of the recovery were performed by ChatGPT (OpenAI), operating through Codex at the user's request.
Summary
On September 22, 2026, Claude Desktop closed during an MSIX update from 2.2553.1.0 to 2.2553.13.0 and could not reopen. Windows displayed “Another program is currently using this file.” Package registration succeeded, but app activation failed with AppModel-Runtime events 215 and 208, HRESULT 0x80070020.
ChatGPT identified the old package's live Job object and queried its actual process membership. Its only remaining members were three
git fsmonitor--daemonprocesses. After their identities were rechecked and those three processes were stopped, Windows immediately destroyed the old AppX container. Claude then launched successfully, without rebooting or signing out.Stopping CoworkVMService alone had already been tested and did not resolve the failure.
Environment
Claude_pzs8sxrjxfjjc.Claude_2.2553.1.0_x64__pzs8sxrjxfjjc.Claude_2.2553.13.0_x64__pzs8sxrjxfjjc.Claude_pzs8sxrjxfjjc!Claude.Observed sequence
| Local time | Evidence |
|---|---|
| 10:20:56 | AppXDeploymentServer event 603 started
RegisterByPackageFamilyNamefor Claude withForceApplicationShutdownOption. The old version's Cowork service was stopped. || 10:20:57 | AppXDeploymentServer event 400 reported successful registration of
2.2553.13.0. || 10:20:58 | AppModel-Runtime events 215/208 reported
0x80070020while creating the updated package's app container/process. || 10:33 and 10:37 | Further launch attempts produced the same failure. |
| 10:46 | After ChatGPT stopped CoworkVMService and verified no Cowork process remained, launching Claude restarted the service but again produced events 215/208. Cowork was stopped again. |
| Before 10:54:41 | An administrator handle scan identified the old Claude Job object; a read-only membership query identified exactly three surviving Git monitors. |
| 10:54:41 | After those three monitors were stopped, event 217 recorded destruction of the old package's container. |
| 10:54:44 | Event 201 recorded successful creation of Claude process 40568 from package
2.2553.13.0. A Claude main window was also observed through the Windows process/window properties. |Diagnostic evidence
The failure was recorded in
Microsoft-Windows-AppModel-Runtime/Admin:Microsoft Sysinternals Handle was downloaded from Microsoft's distribution site and its Microsoft Authenticode signature was verified. An administrator scan found:
ChatGPT used an administrator diagnostic script to duplicate that handle into the diagnostic process with
JOB_OBJECT_QUERYaccess, verify its object name, and callQueryInformationJobObjectwithJobObjectBasicProcessIdList. Only the diagnostic copy of the handle was closed; the service's original handle was not forcibly closed.The complete returned membership was:
| PID | Recorded parent PID | Executable | Command line |
|---|---|---|---|
| 29196 | 18836 |
git.exe|git fsmonitor--daemon run --detach --ipc-threads=8|| 25980 | 17732 |
git.exe|git fsmonitor--daemon run --detach --ipc-threads=8|| 36584 | 45292 |
git.exe|git fsmonitor--daemon run --detach --ipc-threads=8|All three used:
Other Git monitors were running on the machine. The intervention targeted only the three verified members of the old Claude container. The recorded parent PIDs do not, by themselves, establish the full original process ancestry.
Recovery and verification
Immediately before stopping anything, the investigator rechecked each target's PID, parent PID, executable path, process name, and exact daemon command line. Only those three verified processes were terminated.
Windows then recorded:
The updated app had a visible main-window handle titled
Claude. No reboot, sign-out, reinstall, data reset, registry repair, or termination of a Windows service-host process was required. Repository files and Git configuration were not edited as part of the recovery.The PIDs and handle value above are historical evidence from this incident, not reusable instructions for another machine or a later occurrence.
Expected behavior
Claude should reopen successfully after an update. Detached helpers from the previous app instance should not leave the updated app unable to launch. If activation is blocked by surviving container members, diagnostics should identify those members instead of presenting only a generic file-in-use message.
Interpretation and limits
This incident directly establishes that the old Claude Job object contained three surviving Git monitors, and that stopping those members was immediately followed by container destruction and successful launch. This is strong evidence that those monitors were keeping the old container alive and blocking the new version's activation.
The original process-creation calls and job-inheritance flags were not captured. The specific Claude component that originally launched each monitor, the repositories they watched, and the relevant
core.fsmonitorconfiguration were not verified. No claim is made that the exact same cause explains every occurrence of0x80070020.This report documents one observed update failure and one successful targeted recovery. A fresh update was not forced to reproduce it again. Verification covered successful app launch, not every Chat, Code, or Cowork feature. The fix applied here was recovery from the immediate failure; prevention of recurrence requires an application/updater change.
Related upstream report and requested investigation
This locally verified occurrence matches the mechanism described in anthropics/claude-code issue #91763. It adds evidence for the 2.2553.1.0 → 2.2553.13.0 transition and a directly queried old-container membership of three Git monitors. It can be submitted as additional evidence on that issue or cross-referenced in a new report.
Please investigate how detached Git monitors remain associated with the old package's container through forced update shutdown, and provide lifecycle handling that permits reliable updates while respecting intentionally persistent user processes. Consider reporting surviving container members when activation fails so users can recover without restarting Windows.
the three-vs-other-monitors detail is the whole case against fix 1: you only knew which git.exes to kill because job membership named them. break the child out with CREATE_BREAKAWAY_FROM_JOB and that same query returns nothing, so the leaked daemon becomes byte-identical to a user's intentional fsmonitor. now no reaper can target it without risking a live one, and the 0x80070020 that used to flag the leak is gone. loud-but-diagnosable turns into silent-and-unreapable.
Still happening on 2.7032.0.0 (Windows 11 Home 26200, Sept 23, 2026), same mechanism as this report.
0x80070020.git fsmonitor--daemon run --detach --ipc-threads=8processes were still alive, all with dead parents, started 3 days earlier from Claude Code sessions in a repo withcore.fsmonitor=true.User-side workaround until there's a fix:
git config --unset-all core.fsmonitorin any repo you open from the Code tab. On a mid-size repo,git statusstill takes about 130 ms without it.@awakecoding, this may be why your problem came back after you disabled CoworkVMService in your X thread. Stopping that service doesn't release the old container. Killing the leftover git fsmonitor (or other daemon) processes does.
unsetting core.fsmonitor only closes that one vector. the container job doesn't care which child detached, so the first mcp server, node watcher, or LSP that daemonizes re-arms the same 0x80070020 on the next update, with no fsmonitor process to point at.
Another culprit family, and this one is first-party: Claude Code's own Monitor tool.
2.9939.2.0 → 2.9939.4.0(so still present on current builds).RegisterByFamilyName→0x80073D02, 638Running apps: {Claude_pzs8sxrjxfjjc!Claude}with zeroClaude.exealive;-ForceApplicationShutdowndid not help.tail -n +1 -f …\Temp\claude\<project>\…+grep --line-buffered -E …pairs — Monitor watchers from a Code-tab session that had ended; parents dead,IsProcessInJob= true, no package identity.sihost.exerelaunched the new build ~9 s later.Unlike
core.fsmonitor/commit.gpgsign, there is no user-side setting that avoids this one: any Monitor whose session ends without tearing down its pipeline re-arms the next update. So the teardown of Monitor pipelines (kill the whole process group on session end) looks like a needed fix on its own, independent of the breakaway debate above.process-group teardown on session end only covers the graceful exit. ForceApplicationShutdown is the other one, and it kills the parent first, so the hook never runs and tail/grep survive as before. has to be a KILL_ON_JOB_CLOSE job held from spawn, not a handler.
Same mechanism here. Adding two more culprit families, and a way to list the container's members before an update breaks anything, without admin or Handle.exe.
Setup: Windows 11 Pro 26200, Claude Desktop MSIX, update 2.9939.2.0 to 2.16120.0.0. After the update registered, every launch failed with 215/208
0x80070020. Windows answered each failure with its own RegisterByPackageFullName + RepairAppRegistration (event 603). That never helped. A reboot did.1. A third-party Claude Code hook that starts a daemon
I use kcp-commands as a PreToolUse hook on Bash. On the first Bash call it checks for its daemon on localhost:7734. If nothing answers, it runs
nohup java ... -jar kcp-commands-daemon.jar &from Git Bash. That java process (and the nohup.exe above it) inherits the container job, outlives the app and pins the old container.Any hook that lazily starts a background server will do the same. And nobody thinks of their hooks when the app refuses to start.
2. Background servers restarted from a chat
Two of my local servers normally start at sign-in from the Startup folder, outside the app. A Code tab session restarted them during some work. The new copies were started from inside the app, so they joined the container. Parents dead, no package identity, still running days later.
3. Monitor watchers, confirming @Disaer
Six leftover
tail -f | grep --line-bufferedpairs from sessions that had ended days earlier. All still in the job.Listing the members from inside
A Code tab session runs inside the container, so its own tool calls can query their own job. Passing NULL as the job handle to
QueryInformationJobObjectmeans "the job of the calling process". Ask Claude to run this with its PowerShell tool (Windows PowerShell 5.1 is fine):From a normal terminal it returns nothing, because that terminal is not in the container. From inside, it shows exactly what will be left behind when the app goes down. Mine had 76 members. Apart from claude.exe, conhost.exe and the session's own shell, every one of them was a leftover from the list above.
What I do now
explorer.exe <launcher>so the shell becomes the parent. The kcp hook then finds its daemon already running and never spawns one in the container.usr\bintools (nohup, bash, cat, tail, grep) whose parent is dead, plus their children. Known servers get restarted through their launcher instead of just killed.IsProcessInJob(h, NULL)from outside. On Windows 11, Explorer, Chrome, Discord and game launchers were all "in a job" on my machine. It can't separate leftovers from normal programs.But this really needs a fix in the app. Every new tool, hook or MCP server that backgrounds something re-arms the same failure on the next update.
the dead-parent orphan sweep has the same blind spot you hit with IsProcessInJob: a nohup job started from a plain Git Bash outside the app also has a dead parent and no package identity, so the sweep kills it too. your inside-the-job query also only lists the current container, not the old one that's pinning the update.
<html>
<body>
<!--StartFragment--><html><head></head><body><p><strong>AI-assisted follow-up:</strong> this summary was drafted by AI from measurements on my PC</p><p>Following up on my Monitor <code inline="">tail | grep</code> report. Windows 10 Pro 19045; latest measured update: <code inline="">2.19675.1.0 -> 2.26454.0.0</code> on October 7.</p><p>Two corrections to earlier diagnostics:</p><ul><li><p><strong>Nested jobs matter.</strong> <code inline="">QueryInformationJobObject(NULL, ...)</code> on a tool-shell helper returned its innermost job, not necessarily the full Desktop AppX container. Likewise, <code inline="">CREATE_BREAKAWAY_FROM_JOB</code> escaped that inner job but the probe remained in the outer package container. On October 6 it was confirmed as a member of the old September package's container job.</p></li><li><p><strong>Orphan heuristics are unreliable.</strong> A dead-parent sweep included unrelated processes and missed one real container member after its parent's PID had been reused. Querying the specific old package job via a duplicated query-only handle was reliable. Ending the verified survivors produced event 217, after which plain registration succeeded.</p></li></ul><p>The October 7 update also showed that child survival is not uniform:</p>
Time | Observation
-- | --
01:22:29.391 / .569 | Claude spawned two taskkill.exe processes; targets unknown
01:22:30.052 | AppX 603: RegisterByPackageFamilyName, ForceApplicationShutdown
01:22:30.287 | Unelevated helper exited 0xC000013A
01:22:30.346-.370 | JobTerminate events attributed to claude.exe
01:23:00.330621 | JobTerminate: svchost.exe, job 5968, status 1
01:23:00.332865 | Elevated helper exited 1, 2.244 ms later
01:23:00.351 | AppX 646 and Desktop AppX container destruction event 217
<p>External Task Scheduler and WMI control helpers survived. An older breakaway probe also survived, but it came from a previous app instance and is not a valid same-instance control.</p><p>The unelevated helper died during Claude teardown, about 30 seconds before event 646. The elevated helper's timing/status strongly correlate with the later system-side JobTerminate event. I have <strong>not</strong> established the exact console-event sender, mapped job 5968 directly to its members, or explained why some children survive while others are terminated. The JobTerminate container ID also differs from the Desktop container ID in event 217.</p><p>So the current evidence does not support either "all children survive" or "all children are terminated." A fix likely needs separate lifecycle handling for session-owned workers and intentionally persistent processes.</p><p>A paired active-session vs ended-session update test is still a hypothesis, not a result.</p><p>Raw ETW and per-process timestamps are available if useful.</p></body></html><!--EndFragment-->
</body>
</html>
the split you floated needs a label at spawn time, and the worst offender never gets one: fsmonitor is spawned by git, not by Claude, and --detach makes it look persistent. so the 'intentionally persistent' bucket fills up with exactly the processes that pin the container.