[BUG] Windows MSIX Desktop: every process a session spawns leaks kernel nonpaged pool (NtFC) when it opens a path through a junction under AppData (bindflt.sys bug)
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?
Summary
On Windows 11 with the MSIX Claude Desktop, every process spawned by a Claude Code session leaks NtFC nonpaged pool whenever it opens a path through a junction or symlink located under %APPDATA% or %LOCALAPPDATA%. That includes bash, pwsh, git, node, python and hooks. It takes nothing unusual: a uv-managed Python, nvm-windows or pnpm globals put such junctions on the hottest paths.
On a 16 GB machine this reached 2.7 GB in 9 hours of normal agent work. It is freed only by a reboot.
The root cause is a Windows bug in bindflt.sys, the filter behind MSIX AppData virtualisation, reported to Microsoft as Feedback Hub https://aka.ms/AA13ld3q. It stays invisible until Claude Desktop runs every agent child process inside that virtualisation.
Related:
- #95489 is the same tag and the same MSIX AppData redirect, with a different trigger (a retry loop).
- #94198 and #55361 are NtFC growth with Cowork.
- #96722 has
FMfn,FileandIoFE.
Environment
- Windows 11 Home 25H2, build 26200.9457 (ntfs.sys 10.0.26100.9444)
- Claude Desktop MSIX 2.7032.0.0 (
Claude_pzs8sxrjxfjjc), Claude Code 2.1.280 - Only Microsoft file-system minifilters loaded
What it costs in practice (measured per operation, in-session)
- uv-managed Python. uv's minor-version junction
%APPDATA%\uv\python\cpython-3.12-windows-x86_64-noneis used by the~\.local\bintrampolines and by venvs whosehomepoints there. Every stdlib import crosses it: 65 blocks (22.9 KB) perpython -c pass, and about 1,250 blocks (440 KB) per start of a Flask/psycopg app. - The
security-guidanceplugin's hooks startpython3.12through that junction: about 600 blocks (200 KB) per hook run. A Bash tool command containing$(...)fires 7 of them, about 1.5 MB. - PATH entries that are AppData junctions (nvm-windows
.nodejs, Codexbin): 2 × 160 B per command lookup that reaches them. - pnpm globals (
%LOCALAPPDATA%\pnpm\global, a symlink farm): about 25 blocks persfw/socketrun. - It is a true leak, not a cache. 20,000 opens added exactly +20,000 outstanding blocks, and nothing was freed after 10 minutes, on process exit, or on deleting the fixture.
Root cause (WPR pool trace with stacks, tag NtFC)
- The leaking helper.
bindflt.sys(TimeDateStamp 0x4E2AE6D0) has an IRP_MJ_CREATE pre-operation callback that calls a probe helper (RVA 0x2390, return address bindflt+0x2477). The helper re-opens the name in the host silo viaFltCreateFileEx2(FILE_READ_ATTRIBUTES, FILE_OPEN, IO_STOP_ON_SYMLINK). - What it drops. On
STATUS_STOPPED_ON_SYMLINKit returns success without freeing theREPARSE_DATA_BUFFERthat NTFS returned in its stack-localIoStatusBlock.Information. NTFS allocates that buffer at Ntfs+0x144BFD. - The sibling path is correct. The re-issue path in the same callback (bindflt+0x2BBD) hands its buffer on as
TagDatawithSTATUS_REPARSE, andIopParseDevicefrees it. - The trace counts. 2,902 of 2,902 unfreed nonpaged
NtFCblocks carry the probe frame, and 0 of 11,098 freed blocks do. No events were lost: the kernel's ownNtFCcounters moved by exactly the traced totals. - Not involved. NTFS, FltMgr and the I/O manager behave correctly, and UnionFS, wcifs and WdFilter appear on no
NtFCstack. Out of context, the same open is a plain top-level create with no bindflt frames.
Workarounds that measured 0
- uv: set
UV_PYTHON_INSTALL_DIRoutside AppData (e.g.%USERPROFILE%\.uv\python), reinstall the Pythons and rebuild the venvs and uv tools. Python startup went from 65 blocks to 0, and the hook from about 600 to 0. - pnpm: set
PNPM_HOMEoutside AppData and reinstall the globals. Note that pnpm's global config stays in%LOCALAPPDATA%\pnpm\config. - PATH: replace AppData junction entries with their targets, e.g. nvm-windows
.nodejs→.shim. For the Codex CLI, setCODEX_INSTALL_DIRoutside AppData, otherwise everycodex updatere-prepends its AppData bin junction. - Anything launched outside Claude Desktop's package context (a normal terminal) is unaffected.
After these changes, NtFC stayed at 0 outstanding blocks through normal use.
Possible fixes
- Consider launching Claude Code sessions outside the package's AppData virtualisation. One route is
desktop6:FileSystemWriteVirtualization/ flexible virtualisation opt-outs. Another is starting the engine through a broker outside the package context. Either would also end the split-AppData surprises, where config written by agent tools, such as a GnuPG home, lands only inPackages\Claude_…\LocalCache. - Until then, document the leak and the workarounds for Windows users of Claude Desktop, especially the uv one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
What Should Happen?
No memory leak: NtFC staying at 0 outstanding blocks through normal use.
Error Messages/Logs
Steps to Reproduce
Repro (unelevated, about 1 minute, creates nothing)
The script opens the OS-created junction %LOCALAPPDATA%\Microsoft\Windows\Temporary Internet Files (→ INetCache), which every Windows profile has, 2,000 times. It compares that with opening the target directly, reading the NtFC counters via NtQuerySystemInformation(SystemPoolTagInformation). Run it once from a Claude Code session in Claude Desktop (PowerShell tool, or powershell -File from the Bash tool) and once from a normal PowerShell window.
<details>
<summary>ntfc-appdata-junction-repro.ps1</summary>
# NtFC nonpaged-pool leak repro: opens through a junction under %LOCALAPPDATA% from an MSIX-packaged app's
# child process (e.g. a Claude Code session in Claude Desktop) leak one NTFS reparse buffer each (bindflt.sys).
# Uses the OS-created junction "%LOCALAPPDATA%\Microsoft\Windows\Temporary Internet Files" -> INetCache, which
# every Windows profile has, so nothing is created or deleted. Unelevated; Windows PowerShell 5.1 or pwsh 7.
# Run it once inside a Claude Desktop session and once from a normal PowerShell window, and compare.
# Each in-session run leaks roughly 0.5 MB of nonpaged pool until reboot.
param([int] $N = 2000)
Add-Type -TypeDefinition @'
using System;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
public static class NtfcRepro {
[DllImport("ntdll.dll")]
static extern int NtQuerySystemInformation(int infoClass, IntPtr buffer, int length, out int returned);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
static extern SafeFileHandle CreateFileW(string name, uint access, uint share, IntPtr sa, uint disposition, uint flags, IntPtr template);
// SystemPoolTagInformation (22): ULONG Count, then 40-byte SYSTEM_POOLTAG entries from offset 8 on x64.
public static long[] Counters() { // { NonPagedAllocs, NonPagedFrees, NonPagedUsed } for tag "NtFC"
int length = 1 << 20, returned, status;
while (true) {
IntPtr buffer = Marshal.AllocHGlobal(length);
try {
status = NtQuerySystemInformation(22, buffer, length, out returned);
if (status == unchecked((int)0xC0000004)) { length = Math.Max(length * 2, returned + 65536); continue; }
if (status != 0) throw new Exception("NtQuerySystemInformation: 0x" + status.ToString("X8"));
int count = Marshal.ReadInt32(buffer);
for (int i = 0; i < count; i++) {
IntPtr entry = IntPtr.Add(buffer, 8 + i * 40);
if (Marshal.ReadInt32(entry) == 0x4346744E) // 'N','t','F','C' little-endian, case-sensitive
return new long[] { (uint)Marshal.ReadInt32(entry, 24), (uint)Marshal.ReadInt32(entry, 28), Marshal.ReadInt64(entry, 32) };
}
return new long[] { 0, 0, 0 };
} finally { Marshal.FreeHGlobal(buffer); }
}
}
public static int Open(string path, int n) { // FILE_READ_ATTRIBUTES, share all, OPEN_EXISTING, BACKUP_SEMANTICS
int failures = 0;
for (int i = 0; i < n; i++)
using (SafeFileHandle h = CreateFileW(path, 0x80, 7, IntPtr.Zero, 3, 0x02000000, IntPtr.Zero))
if (h.IsInvalid) failures++;
return failures;
}
}
'@
function Measure-Opens([string] $Label, [string] $Path) {
[void] [NtfcRepro]::Open($Path, 50) # warm-up
$before = [NtfcRepro]::Counters()
$failures = [NtfcRepro]::Open($Path, $N)
$after = [NtfcRepro]::Counters()
$allocs = $after[0] - $before[0]; $frees = $after[1] - $before[1]; $bytes = $after[2] - $before[2]
'{0,-34} {1} opens ({2} failed): NtFC allocs/op {3:N2} frees/op {4:N2} leaked {5:N2} blocks/op, {6:N0} bytes total' -f `
$Label, $N, $failures, ($allocs / $N), ($frees / $N), (($allocs - $frees) / $N), $bytes
}
$junction = Join-Path $env:LOCALAPPDATA 'Microsoft\Windows\Temporary Internet Files'
$target = Join-Path $env:LOCALAPPDATA 'Microsoft\Windows\INetCache'
Measure-Opens 'through the AppData junction' $junction
Measure-Opens 'junction target directly (control)' $target
</details>
Result on my machine:
| Where it ran | Through the AppData junction | Target directly (control) |
|---|---|---|
| Claude Code session inside Claude Desktop | 2.00 allocs / 1.00 frees per open → 1 block (272 B) leaked per open, 544,000 B per run | 0 / 0 |
| Normal PowerShell window | 1.00 / 1.00 per open → 0 leaked | 0 / 0 |
Claude Model
None
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
Claude Desktop 2.7032.0 (6c468a)
Platform
Anthropic API
Operating System
Windows
Terminal/Shell
PowerShell
Additional Information
_No response_
Showing cached comments. Read the full discussion on GitHub ↗
3 Comments
Hi @peterlau03, I analyzed the critical issue regarding the kernel nonpaged pool leak (NtFC) when processes spawn in MSIX Desktop and traverse junctions under AppData due to the bindflt.sys bug. I've prepared a robust diagnostic and path-sanitization Python script that automatically detects problematic junction links, monitors handle leaks, and provides workaround isolation wrappers. This tool helps pinpoint the exact paths triggering the memory leak and cleans up local environment configurations. You can integrate it directly into your pipeline to prevent memory exhaustion. Let me know if you need further customization!
Same leak on a second machine, with a few findings that narrow it down.
Environment: Windows 11 Pro 26200.9457,
ntfs.sys10.0.26100.8875,bindflt.sys10.0.26100.9278, Claude Desktop MSIX 2.9939.2.0, Claude Code engine 2.1.281. Microsoft Defender only.1. The engines have no package identity and still leak. The Desktop starts each Claude Code engine (
%APPDATA%\Claude\claude-code\<version>\claude.exe) without package identity:GetCurrentPackageFullNamereturnsAPPMODEL_ERROR_NO_PACKAGEfor the engine and every shell under it. Their opens still leak. Here are 5,000 failed opens (File.Existson a missing file) through%APPDATA%\uv\python\cpython-3.11-windows-x86_64-none, countingNtFCnonpaged allocations minus frees:| Started from | Blocks kept |
|---|---|
| a shell under a Desktop Code session | 5,003, then 5,000 |
|
Win32_Process.Create(WMI) | 3 || a
pwshtab opened bywt.exe| 3 || Explorer | 0 |
Windows Terminal is an MSIX package too, so the leak follows the Claude package's virtualization, not packaging in general.
2. The manifest leaves file-system write virtualization on.
AppxManifest.xmlsetsdesktop6:RegistryWriteVirtualizationtodisabled, butvirtualization:FileSystemWriteVirtualizationstays on, with fourExcludedDirectories(Microsoft\Office\16.0\WEF,Claude-3p,Claude-Data,Claude\logs). Agents' tools therefore write%LOCALAPPDATA%intoPackages\Claude_pzs8sxrjxfjjc\LocalCache, and processes outside the Desktop see a different AppData. Here, a tool's machine-wide registry file exists twice for that reason.3. On this build
%TEMP%is not affected. Failed opens through a new junction directly under%TEMP%, or under%TEMP%\claude\...\scratchpad, kept 0 blocks. New junctions in%LOCALAPPDATA%and in%APPDATA%kept 2 blocks (320 B) per open. #95489 sawTemp\clauderedirected on 26200.9106, where that folder was itself a junction.4. The block size follows the link's reparse data. That fits the unfreed reparse buffer described above:
%LOCALAPPDATA%\Historiajunction, 0xF0: 272 BTemporary Internet Filesjunction, 0xF8: 272 Bpkgsymlink: 2 blocks of 112 BA successful attributes query on an existing file through the uv junction kept nothing.
5. Impact here. Over 15 hours of multi-agent work the pool grew about 7.6 MB an hour, with bursts up to 4.4 MB a minute. An earlier boot reached 1.33 GB after 3.6 days. Each
uv run python -c passkeeps about 73 blocks. With Fast Startup on, only a Restart frees it.Possible fix on the Desktop side: take the engine process tree out of file-system write virtualization, the way the registry already is (
desktop6:FileSystemWriteVirtualization=disabledwith theunvirtualizedResourcescapability the manifest already declares), or start the engines outside the package's virtualization.Same leak, reproduced on a newer build: Windows 11 Home 26H2, build 10.0.26300.9550 (ntfs.sys, bindflt.sys and wcifs.sys all 10.0.26100.9549). So it is not fixed in 26H2.