[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

Status Open
Reported on v2.1.259
Maintainer reply None cached
Activity 20 comments · opened Sep 3, 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?

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-AppxPackage Status = 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:

  1. 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--daemon is the case found here; any --detach/daemonising child (file watchers, language servers, background node processes) will behave the same way.
  2. Before the updater's forced shutdown/registration, enumerate the app's own container job and terminate survivors, or at least fail loudly naming them.
  3. 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

  1. Windows 11, Claude Desktop installed as MSIX (self-updating), standard non-admin user.
  2. Have a git repository with the file-system monitor enabled: git config core.fsmonitor true (Git for Windows 2.55.0 here).
  3. Open the Desktop app's Code tab in that repository and let Claude Code run any git command (e.g. git status). Git spawns git 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 /apps does not list it).
  4. 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.
  5. 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.
  6. Pair AppModel-Runtime events 210 and 217 by container GUID: the previous version's container has a 210 but no 217. The git fsmonitor--daemon process from step 3 is still running and is the only member of that job.
  7. 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.
  8. 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=true set 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 elevated llama-svc.exe console. Here it is an unelevated git fsmonitor--daemon, which any Windows git user with core.fsmonitor enabled will spawn on the first git 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.

View original on GitHub ↗

20 Comments

dannykQuali · 1 month ago

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 update 1.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.log ends there, never relaunches; every launch then failed with AppModel-Runtime 215/208 0x80070020 "error was encountered converting the job".

Pairing 210/217 by container GUID (your method) found exactly one leaked container:

{F6EB323D-...}  Claude_1.40609.1.0_x64__pzs8sxrjxfjjc   created 09-02 16:10:19, no matching 217

Its surviving member was not git — there was no git fsmonitor--daemon process at all. It was a detached Node process that a Claude Code session inside the desktop app had spawned:

PID 34884  node.exe  "C:\Program Files\nodejs\node.exe" C:\Users\<user>\.claude\usage-guard\driver\index.js
created 09-02 17:31:06, parent PID dead, IsProcessInJob = true, no package identity

Killing it produced the 217 for {F6EB323D-...} immediately, and 0x80070020 stopped occurring:

18:06:04  217  Destroyed Desktop AppX container {F6EB323D-...} for package Claude_1.40609.1.0_x64__pzs8sxrjxfjjc
handle64 -a  ->  no \Container_Claude_* job anywhere
next launch  ->  0 x 215/208

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 the CREATE_BREAKAWAY_FROM_JOB fix 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 == true AND 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:

svchost.exe [PID] = svchost -k netsvcs -p -s Appinfo    (Application Information)
  474: Job  \Container_Claude_1.40609.1.0_x64__pzs8sxrjxfjjc-S-1-5-21-<redacted>

svchost.exe [PID] = DcomLaunch/BrokerInfrastructure/PlugPlay/Power/SystemEventsBroker
  ... holds the container job for every *healthy* package (VS Code, Notepad++, Copilot,
      OneDriveSync, Skype, YourPhone, Widgets, WinMerge, WinRAR, CBS)

So handle64.exe -a -p Appinfo and 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> -y fails with "The handle is invalid", and Appinfo cannot be recycled — both Restart-Service Appinfo -Force and sc stop Appinfo silently fail (PID unchanged, SCM still reports RUNNING) despite advertising STOPPABLE / 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:

  • TWinUI 1621: Activation of app Claude_pzs8sxrjxfjjc!Claude attempted. Execution state: ... The operation completed successfully.
  • No Claude.exe process, no event 201, main.log still frozen at the beforeQuitForUpdate line
  • Zero WER reports, zero Crashpad dumps, zero Application Error events — it is not crashing
  • The only container created per activation belongs to the trigger-started cowork-svc.exe
  • Invoke-CommandInDesktopPackage -PackageFamilyName Claude_pzs8sxrjxfjjc -AppId Claude hangs >5 min and launches nothing

The 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 —

SCM 7040  The start type of the Claude service was changed from auto start to disabled.
SCM 7045  A service was installed in the system. Service Name: Claude
AppXDeploymentServer 9648/9650  TerminateSingleService / Successfully terminated service ... CoworkVMService
AppXDeploymentServer 10000      About to service package ... Setting the package state to disabled returned 0x0
AppXDeploymentServer 9626/9622/9623/9625  parsed manifest for install, Evaluate/Commit/RemoveRequest OK

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 looping RepairAppRegistration and surface the real error — would help here too.

Things that report success but change nothing in this state: IPackageDebugSettings::TerminateAllProcesses and ::StopServicing (both return S_OK; GetPackageExecutionState stays 0/UNKNOWN), Add-AppxPackage -Register ...\AppxManifest.xml -DisableDevelopmentMode, and sc stop CoworkVMService before launching. Package health was never in question throughout: Status: Ok, IsPartiallyStaged: False, Get-AppxPackage -AllUsers shows the user as Installed, Get-StartApps lists 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:

New-Item -Path 'HKLM:\SOFTWARE\Policies\Claude' -Force | Out-Null
New-ItemProperty -Path 'HKLM:\SOFTWARE\Policies\Claude' -Name 'disableAutoUpdates' `
  -PropertyType DWord -Value 1 -Force

Booleans must be REG_DWORD 1/0. Read at launch only. Cost is manual updates — running Claude Setup still works and ignores the policy. Note autoUpdaterEnforcementHours alone 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.

m13v · 1 month ago

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.

amitgcoecivil850-crypto · 1 month ago

Hitting this too. Windows 11, dialog referencing Claude_1.46388.2.0_... under C:\Program Files\WindowsApps\. Happened twice so far, each time only a full restart recovered it — matches the mechanism described above.

KE9CFN · 1 month ago

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:

  1. Sysinternals Handle located the old Container_Claude_... job handle in the Appinfo service's process.
  2. We duplicated that handle with query-only access and called QueryInformationJobObject with JobObjectBasicProcessIdList.
  3. The job contained exactly one process: 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, while CoworkVMService was 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-server from 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.

mustalk · 1 month ago

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-agent belongs on that list. It reaches anyone with commit.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.0 to 1.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 times
between 09:09 and 10:21:

215  0x80070020: Cannot create the Desktop AppX container for package
     Claude_1.46388.4.0_x64__pzs8sxrjxfjjc because an error was encountered
     converting the job.

208  0x80070020: Cannot create the process for package
     Claude_1.46388.4.0_x64__pzs8sxrjxfjjc because an error was encountered
     while configuring runtime. [LaunchProcess]

0x80070020 is ERROR_SHARING_VIOLATION, and "converting the job" is the job
object being converted into a server silo.

The leaked container

The old version's container has a Created event and no Destroyed event:

07:14:04  210  Created Desktop AppX container {DFBF4F0A-...} for Claude_1.46388.1.0
07:14:04  211  Added process 34452   (app main process)
07:14:30  211  Added process 4816
07:14:52  658  Marking package {Claude_1.46388.4.0} for deferred registration
                because {Claude_1.46388.1.0} is still running
09:09:18       app quits itself so the deferred registration can complete
09:09:50  215  first 0x80070020, and every launch attempt after it
                (no 217 for {DFBF4F0A} anywhere in the log)

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 needs
no Windows internals to act on:

09:04:53 [updater] Staged version 1.46388.4 is still current (latest: 1.46388.4, lastTarget: null)
09:09:17 [stealth-update] Triggering stealth update after idle timeout
09:09:17 [popout-restore] Saving 0 session + 0 pane popout(s) for next launch
09:09:17 [stealth-relaunch] Saved z-order anchor: 0x10a0f18 (above our 0xe80e5a)
09:09:17 [stealth-relaunch] Saved navigation history (5 entries, active=3)
09:09:18 beforeQuitForUpdate handler fired, going down for update
09:09:18 Windows session ending (close-app) - quitting the app

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:

07:53:36  keyboxd.exe    "C:\Program Files\Git\usr\lib\gnupg\keyboxd.exe" --homedir /c/Users/<user>/.gnupg --daemon
07:53:43  gpg-agent.exe  "C:\Program Files\Git\usr\bin\gpg-agent.exe" --homedir /c/Users/<user>/.gnupg --use-standard-socket --daemon
07:53:43  scdaemon.exe   "C:\Program Files\Git\usr\lib\gnupg\scdaemon.exe" --multi-server

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:

10:49:51  217  Destroyed Desktop AppX container {DFBF4F0A-...} for Claude_1.46388.1.0
10:50:02  210  Created Desktop AppX container {A8732051-...} for Claude_1.46388.4.0
10:50:02  201  Created process 50752 for application Claude_pzs8sxrjxfjjc!Claude

No reboot, no sign-out, no elevation. One detail specific to GnuPG:

End gpg-agent before keyboxd and scdaemon. While it is alive it
respawns its helpers, and the replacements join the same job you are emptying.

Stop-Process -Id <gpg-agent>,<keyboxd>,<scdaemon> -Force

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/09 update on this
machine swapped versions with every container properly destroyed:

04/09 07:33:45  210  Created   {669259F9-...} for Claude_1.40609.1.0
04/09 07:46:01  217  Destroyed {669259F9-...}
04/09 07:46:02  217  Destroyed {476D10C4-...} for Claude_1.40609.1.0
04/09 07:46:02  210  Created   {66925D06-...} for Claude_1.46388.1.0

That session lasted about 12 minutes and spawned nothing that outlived it. The
07/09 session 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 find and head, and now the GnuPG agents are all things a
working 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:

$ev = Get-WinEvent -LogName 'Microsoft-Windows-AppModel-Runtime/Admin' -MaxEvents 5000 |
      Where-Object { $_.Id -in 210,217 -and $_.Message -match 'Claude' }
$now = (Get-AppxPackage -Name Claude).PackageFullName
$made = @{}; $gone = @{}; $pkg = @{}
foreach ($e in $ev) {
    if ($e.Message -notmatch '\{([0-9A-Fa-f-]{36})\}') { continue }
    $g = $matches[1].ToUpper()
    if ($e.Message -match 'for package (\S+?)\.\s*$') { $pkg[$g] = $matches[1] }
    if ($e.Id -eq 210) { if (-not $made.ContainsKey($g)) { $made[$g] = $e.TimeCreated } } else { $gone[$g] = $true }
}
$leak = $made.Keys | Where-Object { -not $gone.ContainsKey($_) -and $pkg[$_] -ne $now }
if (-not $leak) { "No leaked container."; return }

$live = @{}; Get-Process | ForEach-Object { $live[$_.Id] = $true }
foreach ($g in $leak) {
    "LEAKED {$g}  created $($made[$g])  for $($pkg[$g])"
    Get-CimInstance Win32_Process |
        Where-Object { $_.CreationDate -ge $made[$g] -and -not $live.ContainsKey([int]$_.ParentProcessId) } |
        Select-Object ProcessId, Name, CreationDate, CommandLine | Format-Table -AutoSize
}

LEAKED {GUID} followed by a process list means you can recover without
restarting: 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 .ps1
directly; 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-Process line already ordered correctly, and comes with the
fixture 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-agent is busy for something else, but it can
tell whether it started it. Something like:

An earlier version left gpg-agent (PID 33148) running inside its sandbox. Ending it lets Claude start. It restarts on demand, and you may be asked for your GPG passphrase next time you sign a commit.

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.

m13v · 1 month ago

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.

m13v · 1 month ago

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.

m13v · 1 month ago

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.

alexbereznikov · 1 month ago

Confirming the prediction: npx-spawned MCP servers are exactly this class

Your report calls the general case:

git fsmonitor--daemon is the case found here; any --detach/daemonising child (file watchers, language servers, background node processes) will behave the same way.

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, MSIX SignatureKind: Developer, 3 concurrent sessions with ~5 MCP servers each.

This widens the blast radius argument in your report. core.fsmonitor is opt-in, but every MCP server configured in Claude Desktop spawns through cmd.exe → npx → node.exe on 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 with 0x80073D02 ("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_JOB looks right, with one implementation note: MCP servers are launched three levels deep (cmd.exe → npx → node.exe), and the cmd.exe shim 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:

Add-Type -TypeDefinition @'
using System; using System.Runtime.InteropServices; using System.Text;
public static class P {
  [DllImport("kernel32.dll", SetLastError=true)] public static extern IntPtr OpenProcess(int a, bool i, int p);
  [DllImport("kernel32.dll", SetLastError=true)] public static extern bool CloseHandle(IntPtr h);
  [DllImport("kernel32.dll", SetLastError=true)] public static extern bool IsProcessInJob(IntPtr hp, IntPtr hj, out bool r);
  [DllImport("kernel32.dll", CharSet=CharSet.Unicode)] public static extern int GetPackageFullName(IntPtr h, ref int len, StringBuilder sb);
  public static string Info(int pid) {
    IntPtr h = OpenProcess(0x1000, false, pid);
    if (h == IntPtr.Zero) return "open-failed|open-failed";
    bool inJob = false;
    string job = IsProcessInJob(h, IntPtr.Zero, out inJob) ? inJob.ToString() : "query-failed";
    int len = 0; string pkg;
    if (GetPackageFullName(h, ref len, null) == 15700) { pkg = "NONE"; }
    else { var sb = new StringBuilder(len); pkg = GetPackageFullName(h, ref len, sb) == 0 ? sb.ToString() : "error"; }
    CloseHandle(h);
    return job + "|" + pkg;
  }
}
'@
Get-Process | ForEach-Object {
  $i = [P]::Info($_.Id) -split '\|'
  if ($i[0] -eq 'True' -and $i[1] -eq 'NONE') { '{0,-6} {1,-14} inJob={2} identity={3}' -f $_.Id, $_.ProcessName, $i[0], $i[1] }
}

Happy to run further diagnostics on this machine if it helps — it reproduces daily.

m13v · 1 month ago

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.

DanCathers · 22 days ago

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--daemon processes. 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

  • OS: Microsoft Windows 11 Enterprise, 25H2, build 26200.9457, x64.
  • Application: Claude Desktop, Windows MSIX installation.
  • Package family: Claude_pzs8sxrjxfjjc.
  • Previous package: Claude_2.2553.1.0_x64__pzs8sxrjxfjjc.
  • Updated package: Claude_2.2553.13.0_x64__pzs8sxrjxfjjc.
  • Launch identity: Claude_pzs8sxrjxfjjc!Claude.
  • Git executable version: 2.55.0.windows.2.
  • Incident date: 2026-09-22. All times below are America/Los_Angeles (PDT, UTC−07:00).
  • The user reports that they had not configured any Cowork tasks. CoworkVMService was nevertheless installed and running.

Observed sequence

| Local time | Evidence |
|---|---|
| 10:20:56 | AppXDeploymentServer event 603 started RegisterByPackageFamilyName for Claude with ForceApplicationShutdownOption. 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 0x80070020 while 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:

Event 215:
0x80070020: Cannot create the Desktop AppX container for package
Claude_2.2553.13.0_x64__pzs8sxrjxfjjc because an error was encountered
converting the job.

Event 208:
0x80070020: Cannot create the process for package
Claude_2.2553.13.0_x64__pzs8sxrjxfjjc because an error was encountered
while configuring runtime. [LaunchProcess]

Microsoft Sysinternals Handle was downloaded from Microsoft's distribution site and its Microsoft Authenticode signature was verified. An administrator scan found:

svchost.exe  pid: 12044  type: Job  handle: 514
\Container_Claude_2.2553.1.0_x64__pzs8sxrjxfjjc-<user-SID-redacted>

ChatGPT used an administrator diagnostic script to duplicate that handle into the diagnostic process with JOB_OBJECT_QUERY access, verify its object name, and call QueryInformationJobObject with JobObjectBasicProcessIdList. 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:

C:\Program Files\Git\mingw64\libexec\git-core\git.exe

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:

2026-09-22 10:54:41 — Event 217
Destroyed Desktop AppX container
{2FE49E4D-B51D-11F1-B18A-D4548BA2EC92}
for package Claude_2.2553.1.0_x64__pzs8sxrjxfjjc.

2026-09-22 10:54:44 — Event 201
Created process 40568 for application Claude_pzs8sxrjxfjjc!Claude
in package Claude_2.2553.13.0_x64__pzs8sxrjxfjjc. [LaunchProcess]

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.fsmonitor configuration were not verified. No claim is made that the exact same cause explains every occurrence of 0x80070020.

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.

m13v · 22 days ago

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.

TaiwanTammy · 21 days ago

Still happening on 2.7032.0.0 (Windows 11 Home 26200, Sept 23, 2026), same mechanism as this report.

  • Update staged 09:49, applied when I quit the app around 16:17. Every relaunch after that failed with events 215/208, 0x80070020.
  • Three orphaned git fsmonitor--daemon run --detach --ipc-threads=8 processes were still alive, all with dead parents, started 3 days earlier from Claude Code sessions in a repo with core.fsmonitor=true.
  • Killing just those three fixed it right away: the next launch worked, with no reboot and no admin rights needed. Two relaunches a few seconds before that had failed, even though CoworkVMService had just restarted.
  • The same failure happened on the previous two updates too (2.110.1.0 on 9/17 and 2.2553.0.0 on 9/18), so this seems to hit most updates once a daemon has leaked.

User-side workaround until there's a fix: git config --unset-all core.fsmonitor in any repo you open from the Code tab. On a mid-size repo, git status still 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.

m13v · 21 days ago

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.

Disaer · 16 days ago

Another culprit family, and this one is first-party: Claude Code's own Monitor tool.

  • Windows 10 Pro 19045, Claude Desktop 2.9939.2.0 → 2.9939.4.0 (so still present on current builds).
  • Symptom was the register-time variant: 5 × event 658, then every RegisterByFamilyName → 0x80073D02, 638 Running apps: {Claude_pzs8sxrjxfjjc!Claude} with zero Claude.exe alive; -ForceApplicationShutdown did not help.
  • Old container (created 09-27 13:22, no 217) was pinned by two 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.
  • Killed the 4 processes → 217 for that container in the same second → first plain register succeeded; sihost.exe relaunched 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.

m13v · 16 days ago

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.

Fuzzied · 10 days ago

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-buffered pairs 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 QueryInformationJobObject means "the job of the calling process". Ask Claude to run this with its PowerShell tool (Windows PowerShell 5.1 is fine):

Add-Type -Namespace W -Name J -MemberDefinition '[DllImport("kernel32.dll")] public static extern bool QueryInformationJobObject(IntPtr h, int c, IntPtr i, int l, out int r);'
$len = 8 + 8*2048; $buf = [Runtime.InteropServices.Marshal]::AllocHGlobal($len); $r = 0
[void][W.J]::QueryInformationJobObject([IntPtr]::Zero, 3, $buf, $len, [ref]$r)   # 3 = JobObjectBasicProcessIdList
$n = [Runtime.InteropServices.Marshal]::ReadInt32($buf, 4)
0..($n-1) | ForEach-Object { [Runtime.InteropServices.Marshal]::ReadInt64($buf, 8 + 8*$_) } |
  ForEach-Object { Get-CimInstance Win32_Process -Filter "ProcessId=$_" } |
  Format-Table ProcessId, Name, ParentProcessId, CommandLine -AutoSize

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

  • Start daemons outside the app. At sign-in from the Startup folder, or relaunched with 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.
  • Clean up orphans around an update. Git for Windows usr\bin tools (nohup, bash, cat, tail, grep) whose parent is dead, plus their children. Known servers get restarted through their launcher instead of just killed.
  • Don't filter on 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.
  • If it's already stuck, signing out of Windows and back in clears it faster than a reboot.

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.

m13v · 9 days ago

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.

Disaer · 7 days ago

<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 -&gt; 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>

m13v · 6 days ago

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.