[BUG] MSIX write redirection is misdetected as a junction-planting attack (`PlantDetectedError`), breaking Cowork VM SDK install on every app update

Status Fixed / completed
Reported on v2.1.222
Maintainer reply None cached
Activity 8 comments · opened Aug 7, 2026 · closed Aug 10, 2026

<html>
<body>
<!--StartFragment--><html><head></head><body><h3>Preflight Checklist</h3>
<ul>
<li>[x] I have searched existing issues and this hasn't been reported yet</li>
<li>[x] This is a single bug report</li>
<li>[x] I am using the latest version</li>
</ul>
<h3>What's Wrong?</h3>
<p>On a Windows MSIX-packaged Claude Desktop install, the app's private-directory hardening check rejects its own package write-redirection as a symlink-planting attack. Every subsystem that creates a <em>new</em> directory under <code>%APPDATA%\Claude</code> fails with:</p>
<pre><code>PlantDetectedError: Private dir leaf redirects (junction/substitute-name plant): &lt;path&gt;
</code></pre>
<p>For the Cowork VM SDK this surfaces to the user as <strong>"Download failed. Check your internet connection and try again."</strong> and a prompt to install the workspace SDK manually. The message is misleading — there is no network involvement in the failing code path.</p>
<p><strong>There is no junction on disk.</strong> This is a false positive:</p>

Check | Result
-- | --
fsutil reparsepoint query "$env:APPDATA\Claude\claude-code-vm" | Error 4390: not a reparse point
Recursive scan for ReparsePoint attribute under %APPDATA%\Claude (depth 2) | 0 hits

<p>The audit-logger case is the one I'd flag as most serious: audit entries are being <strong>silently dropped</strong> for the whole session, which is a security-relevant failure caused by a security check.</p>
<h3>What Should Happen?</h3>
<p>The guard should not treat the app's own MSIX redirection as hostile. Resolve the redirect target and accept it when it lands inside the package's own container (<code>%LOCALAPPDATA%\Packages\&lt;PackageFamilyName&gt;\LocalCache\...</code>), rejecting only redirects that escape it. Alternatively, skip the check entirely when running packaged, since the container is already the trust boundary.</p>
<p>Two lesser fixes worth doing regardless:</p>
<ol>
<li>Do not report <code>PlantDetectedError</code> as <code>Download failed. Check your internet connection and try again.</code> The current message sends users to diagnose their network, proxy, and firewall for a purely local path-resolution refusal.</li>
<li>Do not let a failed <code>[audit]</code> logger init silently drop entries.</li>
</ol>
<h3>Error Messages/Logs</h3>
<p><code>%APPDATA%\Claude\logs\main.log</code>:</p>
<pre><code>2026-08-07 23:05:14 [error] [ClaudeCodeManager-VM] Download attempt 2/3 failed J [PlantDetectedError]: Private dir leaf redirects (junction/substitute-name plant): C:\Users\&lt;user&gt;\AppData\Roaming\Claude\claude-code-vm\2.1.222
at $n (...\app.asar\.vite\build\index.chunk-CWSnTt4v.js:12:14339)
at async er (...\app.asar\.vite\build\index.chunk-CWSnTt4v.js:12:14674)
at async Object.tr (...\app.asar\.vite\build\index.chunk-CWSnTt4v.js:12:15304)
at async ae.downloadBinaryForTarget (...\app.asar\.vite\build\index2.chunk-6F17y8zy.js:1:11029)
at async ae.prepareForTarget (...\app.asar\.vite\build\index2.chunk-6F17y8zy.js:1:14710)
at async ae.prepareForVM (...\app.asar\.vite\build\index2.chunk-6F17y8zy.js:1:23002)
at async Promise.all (index 1)
at async Ut (...\app.asar\.vite\build\index.chunk-BePYlYru.js:10:1470)
at async Object.startVM (...\app.asar\.vite\build\index.chunk-aUP09L-a.js:78:45096)
name: 'PlantDetectedError',
code: undefined
2026-08-07 23:05:18 [error] [ClaudeCodeManager-VM] All download attempts failed
2026-08-07 23:04:43 [error] [LocalPlugins] Failed to sync remote plugins: Private dir leaf redirects (junction/substitute-name plant): ...\local-agent-mode-sessions\&lt;GUID&gt;\&lt;GUID&gt;\rpm
2026-08-07 23:23:51 [error] Failed to save pasted file: Private dir leaf redirects (junction/substitute-name plant): ...\Claude\pending-uploads
2026-08-07 23:25:08 [error] [audit] logger init failed — dropping entries { storageDir: '...\local-agent-mode-sessions\&lt;GUID&gt;\&lt;GUID&gt;\local_&lt;GUID&gt;', error: 'Private dir leaf redirects (junction/substitute-name plant): ...' }
</code></pre>
<p><code>%APPDATA%\Claude\logs\cowork_vm_node.log</code> — the same condition after it reaches the user-facing layer:</p>
<pre><code>2026-08-05 16:21:14 [info] [VM:steps] download_and_sdk_prepare started
2026-08-05 16:21:17 [error] [VM:steps] download_and_sdk_prepare failed (3041ms): Download failed. Check your internet connection and try again.
2026-08-05 16:21:17 [error] [VM:start] Startup failed: Error: Download failed. Check your internet connection and try again.
2026-08-05 16:21:17 [info] [deleteVMBundle] Deleting reinstall files from ...\vm_bundles\claudevm.bundle
</code></pre>
<p>Note the failure duration is <code>3023</code>–<code>3070 ms</code> across every one of ~110 occurrences — deterministic local refusal, not a network timeout distribution.</p>
<p>Eventual success on the same day, unchanged environment:</p>
<pre><code>2026-08-07 23:18:05 [info] [VM:steps] sdk_install started
2026-08-07 23:18:05 [info] [postConnect] Installing SDK: subpath=c/Users/&lt;user&gt;/AppData/Local/Packages/Claude_pzs8sxrjxfjjc/LocalCache/Roaming/Claude/claude-code-vm, version=2.1.222
2026-08-07 23:18:07 [info] [VM:steps] sdk_install completed (1956ms)
</code></pre>
<h3>Steps to Reproduce</h3>
<ol>
<li>Install Claude Desktop on Windows via the MSIX installer.</li>
<li>Let the app update to a new version (this creates a new <code>claude-code-vm\&lt;version&gt;</code> leaf).</li>
<li>Open a Cowork session.</li>
<li>Startup fails with "Download failed. Check your internet connection and try again." and prompts for manual workspace/SDK installation.</li>
<li><code>%APPDATA%\Claude\logs\main.log</code> shows <code>PlantDetectedError</code> for <code>%APPDATA%\Claude\claude-code-vm\&lt;version&gt;</code>.</li>
<li>Confirm no reparse point exists:
<pre><code class="language-powershell">fsutil reparsepoint query "$env:APPDATA\Claude\claude-code-vm"Get-ChildItem "$env:APPDATA\Claude" -Force -Directory -Recurse -Depth 2 -ErrorAction SilentlyContinue | Where-Object { $_.Attributes -band [IO.FileAttributes]::ReparsePoint }
</code></pre>Both return nothing.</li>
</ol>
<h3>Is this a regression?</h3>
<p>Yes. The same install path worked before the <code>PlantDetectedError</code> guard was introduced. A related earlier form of the check appears in <code>cowork_vm_node.log</code> on 2026-06-13 as <code>configure: path ...\LocalCache\Roaming\Claude\vm_bundles is a symlink or junction, refusing to open</code>.</p>
<h3>Claude Code Version</h3>
<p>SDK <code>2.1.222</code> · app package <code>Claude_1.26832.0.0_x64__pzs8sxrjxfjjc</code> (MSIX)</p>
<h3>Platform</h3>
<p>Claude Desktop (Cowork)</p>
<h3>Operating System</h3>
<p>Windows 11, version 25H2 (OS build 26200.8875)</p>
<h3>Terminal/Shell</h3>
<p>PowerShell</p>
<h3>Additional Information</h3>
<p>Installer type: MSIX (<code>C:\Program Files\WindowsApps\Claude_1.26832.0.0_x64__pzs8sxrjxfjjc</code>). Package family name <code>Claude_pzs8sxrjxfjjc</code>.
Personal (non-domain-joined) machine. No folder redirection policy, no OneDrive redirection of <code>AppData</code>, and no manually created junctions under <code>%APPDATA%\Claude</code> — verified as above. So the redirection the guard is reacting to can only be the package's own MSIX virtualization.</p>
<p>Antivirus: Norton Security (consumer). No enterprise EDR agent.</p></body></html><!--EndFragment-->
</body>
</html>

View original on GitHub ↗

4 Comments

gregblanchette-66 · 23 days ago

Independent confirmation on a second machine — same package (Claude_1.26832.0.0_x64__pzs8sxrjxfjjc, SDK 2.1.222), Windows 11 Home 26200.8973, non-domain, no folder redirection, no OneDrive AppData redirection. Verified zero reparse points: Get-Item attributes checked on every path segment from C:\Users down to claude-code-vm, plus fsutil reparsepoint query — all plain directories, and the 2.1.222 leaf doesn't exist yet when the check fires.

Regression dating: first PlantDetectedError in my main.log is 2026-08-06 14:08:36 CT, coinciding exactly with an app auto-update — zero occurrences before that timestamp. Persisted through the next package update (installed 2026-08-06 23:08) and every restart since. All 3 download attempts fail in under 1 second — consistent with the deterministic local refusal noted in the report, no network involvement.

Two additional impact surfaces not yet in this thread:

1. Cowork-native scheduled tasks die silently. When the VM stops booting, every Cowork-native scheduled task stops executing — in my case 15+ tasks dead for ~30 hours, discovered only via a missing report output. The only trace is [ScheduledTasks] startVM failed: Error: Download failed. Check your internet connection and try again. in main.log. No user-facing alert of any kind. Whatever the fix for the guard itself, scheduled-task failure deserves a visible notification path.

2. The same path-identity confusion breaks the Claude-in-Chrome native messaging bridge. The app registered the native host using virtualized paths in both places Chrome looks:

  • HKCU\Software\Google\Chrome\NativeMessagingHosts\com.anthropic.claude_browser_extension (default) → ...\AppData\Local\Packages\Claude_pzs8sxrjxfjjc\LocalCache\Roaming\Claude\ChromeNativeHost\com.anthropic.claude_browser_extension.json
  • that manifest's "path"...\LocalCache\Roaming\Claude\ChromeNativeHost\chrome-native-host.exe

Neither file exists at the LocalCache location (the directory is empty). The real manifest + exe live in %APPDATA%\Claude\ChromeNativeHost\. Chrome — an unpackaged process — resolves the LocalCache path literally, finds nothing, and the native host never spawns; the desktop app side just loops No Chrome extension connected after discovery while reporting itself connected=true, authenticated=true to the relay.

Manual repair, verified working today: point the registry default value at the real manifest path, change the manifest "path" to the real chrome-native-host.exe location, restart Chrome. chrome-native-host.exe spawns immediately and the extension connects.

The guard's false positive and the registration writing virtualized paths into externally-consumed pointers (registry + manifest) look like two symptoms of one root: the packaged app and unpackaged consumers (Chrome; the plant check comparing pre-/post-resolution paths) disagree about which of the two AppData views is canonical.

kamilgenc60 · 22 days ago

Confirming this on a second machine, with two additional data points.

Still failing on SDK 2.1.222. This issue was filed while 2.1.221 was current. The app has since moved on to 2.1.222 and it is blocked identically, so this is not a bad build of one SDK version — the check rejects the destination regardless of which version is being fetched. Any future SDK release will hit the same wall.

--- LocalCache\Roaming\Claude\claude-code-vm ---
2.1.219   275,004,464 bytes   2026-07-25   OK
2.1.221             0 bytes   2026-08-05   blocked
2.1.222             0 bytes   2026-08-08   blocked
.sdk-version                               contents: "2.1.219"

Deleting the failed download directories does not help — tested. With Claude Desktop fully closed, all four zero-byte version directories (claude-code\2.1.221, claude-code\2.1.222, claude-code-vm\2.1.221, claude-code-vm\2.1.222) and vm_bundles\claudevm.bundle\.auto_reinstall_attempted were deleted. The intact 2.1.219 installs were left untouched. On restart the app recreated the version directories (still zero-byte), recreated the marker, and failed identically:

2026-08-09 00:31:46 [error] [ClaudeCodeManager-VM] Download attempt 3/3 failed J [PlantDetectedError]:
Private dir leaf redirects (junction/substitute-name plant):
C:\Users\<user>\AppData\Roaming\Claude\claude-code-vm\2.1.222

This confirms the check fires on path resolution, not on the state or contents of the destination directory — consistent with fsutil reparsepoint query returning ERROR_NOT_A_REPARSE_POINT for these paths. It also means there is no user-side workaround: the destination path is chosen by the app, the virtualization is imposed by Windows, and the check rejects the combination. None of the three is under the user's control.

The .auto_reinstall_attempted guard makes this self-perpetuating: it is recreated after every failed attempt, so the error dialog's "reinstall the workspace" advice cannot succeed even after manual cleanup.

Environment: Claude Desktop 1.25927.0.0, package Claude_pzs8sxrjxfjjc, Windows 11 Home 25H2 (build 26200.8973), VirtualMachinePlatform enabled, vmcompute/hns running, VM bundle intact (rootfs.vhdx 7,688 MB), 776 GB free disk. Failure is consistently ~3 s, far too fast for a network timeout.

kulaw327 · 21 days ago

<html>
<body>
<!--StartFragment--><html><head></head><body><p>Third machine, same regression — but it ends in a <strong>different final state</strong> that may be worth more attention than the block itself: the failure path terminated in something the app treats as success.</p>
<p><strong>Same symptoms as reported.</strong> Windows 11 25H2 (26200.8875), package <code>Claude_1.26832.0.0_x64__pzs8sxrjxfjjc</code>, personal non-domain machine, no folder redirection, zero reparse points under <code>%APPDATA%\Claude</code> (depth 2) <em>and</em> under the package <code>LocalCache</code> (depth 3). 691 <code>PlantDetectedError</code> occurrences in a 21-minute window across five subsystems.</p>
<h3>What happened after the download attempts failed</h3>
<p>All logged download attempts for <code>2.1.222</code> failed, and then the version was marked verified anyway.</p>

Time (2026-08-07) | Event
-- | --
23:05:13 – 23:07:15 | [ClaudeCodeManager-VM] Download attempt N/3 failed × 12 (4 rounds), all PlantDetectedError on ...\claude-code-vm\2.1.222
23:07:15 | All download attempts failed (last one)
23:11:30 | 2.1.222\.verified created
23:11:44 onward | app restarted 5 more times — zero further download attempts logged
23:18:07 | [VM:steps] sdk_install completed (1956ms)

<p>There is <strong>no successful <code>downloadBinaryForTarget</code> anywhere in the log.</strong> Note that <code>sdk_install</code> (installing into the VM) is a different step from <code>downloadBinaryForTarget</code> (fetching the host-side binary) — the former succeeding does not mean the latter did.</p>
<h3>The artifact</h3>
<pre><code>2026-07-25 10:50:13 275,004,400 2.1.219\claude
2026-08-06 20:15:48 288,705,544 2.1.221\claude
2026-08-06 20:19:18 64 2.1.221\.verified
2026-08-06 20:15:48 288,705,544 2.1.222\claude &lt;- same size AND same mtime as 2.1.221
2026-08-07 23:11:30 64 2.1.222\.verified &lt;- written after all 2.1.222 downloads failed
2026-08-09 17:27:40 7 .sdk-version -&gt; "2.1.222"
</code></pre>
<p>SHA-256 of the two binaries:</p>
<pre><code>60DB8E88D42C24B5199C92CFD56EC88370C510C3789C6F364AF748354F087ADA 2.1.221\claude
60DB8E88D42C24B5199C92CFD56EC88370C510C3789C6F364AF748354F087ADA 2.1.222\claude
</code></pre>
<p>Byte-identical. And the <code>.verified</code> markers differ from each other while neither equals the binary's hash:</p>
<pre><code>2.1.221\.verified: d8a43ae3cea71283d1d228937735c47f5b533829ce7a725ee081a5a72510ab41
2.1.222\.verified: 913436d3e6b4707a022136cf4cdd35894e7b7b9f449a9ffa5caacc94404cacf1
file SHA-256: 60db8e88d42c24b5199c92cfd56ec88370c510c3789c6f364af748354f087ada
</code></pre>
<p>So <code>.verified</code> does not bind to the content of <code>claude</code>. Two directories holding the identical file carry different markers, and neither marker is that file's hash.</p>
<h3>Why I think this matters</h3>
<p>The app advanced <code>.sdk-version</code> to <code>2.1.222</code> and stopped retrying, on a version whose every logged download attempt failed, with a marker that does not attest to the artifact present. Cowork has been running normally since. Where <code>kamilgenc60</code> sees zero-byte directories and a permanent block, this machine got a silent pass instead — the same defect, different termination.</p>
<p>If two SDK versions can ever ship different binaries, this path would run the older artifact under the newer version label without surfacing anything.</p>
<p><strong>Alternative explanations I could not rule out</strong>, offered so they can be eliminated quickly:</p>
<ol>
<li><code>2.1.221</code> and <code>2.1.222</code> may genuinely ship an identical <code>claude</code> binary, in which case the matching hash is expected and only the <code>.verified</code> timing is anomalous.</li>
<li>Archive extraction preserves mtimes, so the shared <code>2026-08-06 20:15:48</code> mtime does not by itself prove the file was copied rather than re-extracted.</li>
</ol>
<p>Neither explains a <code>.verified</code> written at 23:11:30 when the last download attempt failed at 23:07:15 and none succeeded.</p>
<p>It would help to know what <code>.verified</code> is expected to contain, and whether anything re-validates a version directory's contents against the version being requested.</p></body></html><!--EndFragment-->
</body>
</html>

kamilgenc60 · 21 days ago

Data point from the blocked machine referenced above, since it isolates where the anomaly is: on a machine where the download never succeeds, no .verified is written and .sdk-version is not advanced.

claude-code\2.1.217      248 MB   no .verified
claude-code\2.1.219      254 MB   .verified present (64 bytes)
claude-code\2.1.222        0 MB   no .verified
claude-code-vm\2.1.219   263 MB   .verified present (64 bytes)
claude-code-vm\2.1.222     0 MB   no .verified
claude-code-vm\.sdk-version       still "2.1.219"

So on this machine the failure is at least self-consistent: nothing is marked verified, the version label is not advanced, and the app keeps retrying on every start. The silent pass appears specific to the path taken on the third machine, not a general consequence of PlantDetectedError.

Worth noting for whoever picks this up: the 2.1.222 directories here were recreated after a manual cleanup (all zero-byte version directories and .auto_reinstall_attempted deleted with the app closed), so their empty state is current, not a leftover from the first failure.

No opinion on what .verified should contain — only that its absence here lines up with downloads that never completed.

Showing cached comments. Read the full discussion on GitHub ↗