Skip to content

Commit c4a8164

Browse files
yeldarbyclaude
andcommitted
Probe MSIX-private AppData for claude.exe (elevated-shell case)
Field bug: detection still failed for a user whose only claude.exe was under %APPDATA%\Claude\claude-code\<ver>\claude.exe. Diagnostics showed the probe found "does NOT exist" -- yet the file was clearly accessible from another process. Cause: Claude Desktop ships as an MSIX package (Claude_<hash>) and the OS only applies the %APPDATA%\Claude redirect to processes inside the MSIX container. A regular PowerShell session, not a child of the MSIX app, sees the redirect target as empty/absent and has to read the underlying real path at %LOCALAPPDATA%\Packages\Claude_<hash>\LocalCache\Roaming\Claude\claude-code\<ver>\claude.exe. Resolve-RfClaudeCliPath now builds a list of candidate code roots: 1) %APPDATA%\Claude\claude-code (visible from MSIX context) 2) %LOCALAPPDATA%\Packages\Claude_*\LocalCache\Roaming\Claude\claude-code (real filesystem path -- visible from any process) APPDATA wins when both resolve, since it's the user-facing path. The same logic feeds Show-RfDetectDiagnostics so future failure dumps list every probe attempted. Two new Pester cases cover (a) the MSIX-private path being the only hit and (b) the APPDATA-wins-over-MSIX precedence. Required tightening isolation in the Test-RfHostClaudeCodeCli Describe block so LOCALAPPDATA gets pointed at the isolated home too -- otherwise the new probe finds the developer's real install during tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 36dd48a commit c4a8164

2 files changed

Lines changed: 86 additions & 17 deletions

File tree

installer/lib/detect.ps1

Lines changed: 51 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,41 @@ function Resolve-RfClaudeCliPath {
6060
$appdata = if ($env:APPDATA) { $env:APPDATA } else { Join-Path $HOME 'AppData\Roaming' }
6161
$localApp = if ($env:LOCALAPPDATA) { $env:LOCALAPPDATA } else { Join-Path $HOME 'AppData\Local' }
6262

63-
# Versioned tree: %APPDATA%\Claude\claude-code\<semver>\claude.exe.
64-
# This is what both the standalone Anthropic Windows installer and
65-
# the Claude Desktop MSIX (Claude_<...>) populate. Pick the newest
66-
# semver dir. Fall back to lex-sort if [Version] parse fails for an
67-
# unusual name (e.g. nightly/preview build).
68-
$codeRoot = Join-Path $appdata 'Claude\claude-code'
69-
if (Test-Path -LiteralPath $codeRoot) {
63+
# Versioned tree: <root>\claude-code\<semver>\claude.exe.
64+
# Three possible roots on Windows, all checked in order:
65+
#
66+
# a) %APPDATA%\Claude\claude-code\
67+
# What both the standalone Anthropic installer and the
68+
# Claude Desktop MSIX populate -- visible from any process
69+
# that is a child of the MSIX app (the path is virtualized
70+
# back via the WindowsApps redirect), and from any process
71+
# outside the MSIX context IF the install also wrote a real
72+
# reparse point there.
73+
#
74+
# b) %LOCALAPPDATA%\Packages\Claude_*\LocalCache\Roaming\Claude\claude-code\
75+
# The REAL filesystem location that the MSIX redirect points
76+
# to. When a user launches a fresh PowerShell (not a child
77+
# of the MSIX app), (a) typically returns "does not exist"
78+
# because the redirect only applies to processes inside the
79+
# MSIX container, and you have to fall through to here.
80+
# This was the bug reported in the field.
81+
#
82+
# c) %LOCALAPPDATA%\Programs\claude\claude.exe and friends --
83+
# Squirrel installer / future paths, kept for completeness.
84+
$codeRoots = @()
85+
$codeRoots += (Join-Path $appdata 'Claude\claude-code')
86+
$msixPackages = Join-Path $localApp 'Packages'
87+
if (Test-Path -LiteralPath $msixPackages) {
88+
try {
89+
Get-ChildItem -LiteralPath $msixPackages -Directory -Filter 'Claude_*' -ErrorAction Stop |
90+
ForEach-Object {
91+
$codeRoots += (Join-Path $_.FullName 'LocalCache\Roaming\Claude\claude-code')
92+
}
93+
} catch { }
94+
}
95+
96+
foreach ($codeRoot in $codeRoots) {
97+
if (-not (Test-Path -LiteralPath $codeRoot)) { continue }
7098
$versioned = Get-ChildItem -LiteralPath $codeRoot -Directory -ErrorAction SilentlyContinue |
7199
Where-Object {
72100
$exe = Join-Path $_.FullName 'claude.exe'
@@ -357,12 +385,22 @@ function Show-RfDetectDiagnostics {
357385
Write-RfDim " `$env:RF_TEST_NO_DETECT_APPS: $noDetect"
358386
Write-RfDim " whoami: $(try { whoami 2>$null } catch { '(failed)' })"
359387

360-
if ($env:APPDATA) {
361-
$codeRoot = Join-Path $env:APPDATA 'Claude\claude-code'
362-
if (Test-Path -LiteralPath $codeRoot) {
363-
Write-RfDim " Probe $codeRoot — EXISTS, subdirs:"
388+
$probes = @()
389+
if ($env:APPDATA) { $probes += (Join-Path $env:APPDATA 'Claude\claude-code') }
390+
if ($env:LOCALAPPDATA) {
391+
$pkgRoot = Join-Path $env:LOCALAPPDATA 'Packages'
392+
if (Test-Path -LiteralPath $pkgRoot) {
393+
try {
394+
Get-ChildItem -LiteralPath $pkgRoot -Directory -Filter 'Claude_*' -ErrorAction Stop |
395+
ForEach-Object { $probes += (Join-Path $_.FullName 'LocalCache\Roaming\Claude\claude-code') }
396+
} catch { }
397+
}
398+
}
399+
foreach ($probe in $probes) {
400+
if (Test-Path -LiteralPath $probe) {
401+
Write-RfDim " Probe $probe — EXISTS, subdirs:"
364402
try {
365-
foreach ($d in (Get-ChildItem -LiteralPath $codeRoot -Directory -ErrorAction Stop)) {
403+
foreach ($d in (Get-ChildItem -LiteralPath $probe -Directory -ErrorAction Stop)) {
366404
$exe = Join-Path $d.FullName 'claude.exe'
367405
$hasExe = Test-Path -LiteralPath $exe -PathType Leaf
368406
Write-RfDim " - $($d.Name) (claude.exe present: $hasExe)"
@@ -371,7 +409,7 @@ function Show-RfDetectDiagnostics {
371409
Write-RfDim " (enumeration failed: $($_.Exception.Message))"
372410
}
373411
} else {
374-
Write-RfDim " Probe $codeRoot — does NOT exist"
412+
Write-RfDim " Probe $probe — does NOT exist"
375413
}
376414
}
377415

tests/pester/Detect.Tests.ps1

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,33 @@ Describe 'Resolve-RfClaudeCliPath' {
7676
$resolved | Should -Be (Join-Path $verDir 'claude.exe')
7777
}
7878

79+
It 'finds claude inside the MSIX-private AppData (the elevated-shell case)' -Skip:(-not $script:rfTestIsWindows) {
80+
# Field bug: a regular PowerShell session (not a child of the
81+
# MSIX-containerized Claude Desktop) doesn't see the
82+
# %APPDATA%\Claude redirect, so the install only resolves via the
83+
# real path under %LOCALAPPDATA%\Packages\Claude_<hash>\LocalCache\.
84+
# Don't create the APPDATA copy here — only the MSIX-private copy.
85+
$pkgDir = Join-Path $env:LOCALAPPDATA 'Packages\Claude_pzs8sxrjxfjjc\LocalCache\Roaming\Claude\claude-code\2.1.138'
86+
New-Item -ItemType Directory -Path $pkgDir -Force | Out-Null
87+
New-Item -ItemType File -Path (Join-Path $pkgDir 'claude.exe') -Force | Out-Null
88+
$resolved = Resolve-RfClaudeCliPath -Force
89+
$resolved | Should -Be (Join-Path $pkgDir 'claude.exe')
90+
}
91+
92+
It 'APPDATA probe wins over MSIX-package probe when both exist' -Skip:(-not $script:rfTestIsWindows) {
93+
# Both layouts present (e.g. a process inside MSIX context sees
94+
# the redirect AND can also reach the underlying file). The
95+
# APPDATA path is the canonical user-facing one, so prefer it.
96+
$appdataVer = Join-Path $env:APPDATA 'Claude\claude-code\2.1.138'
97+
New-Item -ItemType Directory -Path $appdataVer -Force | Out-Null
98+
New-Item -ItemType File -Path (Join-Path $appdataVer 'claude.exe') -Force | Out-Null
99+
$pkgVer = Join-Path $env:LOCALAPPDATA 'Packages\Claude_pzs8sxrjxfjjc\LocalCache\Roaming\Claude\claude-code\2.1.138'
100+
New-Item -ItemType Directory -Path $pkgVer -Force | Out-Null
101+
New-Item -ItemType File -Path (Join-Path $pkgVer 'claude.exe') -Force | Out-Null
102+
$resolved = Resolve-RfClaudeCliPath -Force
103+
$resolved | Should -Be (Join-Path $appdataVer 'claude.exe')
104+
}
105+
79106
It 'picks the highest semver dir when multiple versions exist' -Skip:(-not $script:rfTestIsWindows) {
80107
$root = Join-Path $env:APPDATA 'Claude\claude-code'
81108
foreach ($v in @('1.0.0', '2.1.138', '2.1.9', '2.10.0')) {
@@ -142,13 +169,17 @@ Describe 'Test-RfHostClaudeCodeCli' {
142169
BeforeEach {
143170
$script:rfHome = New-RfIsolatedHome
144171
$Script:RfClaudeCliPath = $null
145-
$script:origAppData = $env:APPDATA
146-
$env:APPDATA = Join-Path $script:rfHome 'AppData/Roaming'
147-
New-Item -ItemType Directory -Path $env:APPDATA -Force | Out-Null
172+
$script:origAppData = $env:APPDATA
173+
$script:origLocalAppData = $env:LOCALAPPDATA
174+
$env:APPDATA = Join-Path $script:rfHome 'AppData/Roaming'
175+
$env:LOCALAPPDATA = Join-Path $script:rfHome 'AppData/Local'
176+
New-Item -ItemType Directory -Path $env:APPDATA -Force | Out-Null
177+
New-Item -ItemType Directory -Path $env:LOCALAPPDATA -Force | Out-Null
148178
Remove-Item Env:RF_TEST_NO_DETECT_APPS -ErrorAction SilentlyContinue
149179
}
150180
AfterEach {
151-
$env:APPDATA = $script:origAppData
181+
$env:APPDATA = $script:origAppData
182+
$env:LOCALAPPDATA = $script:origLocalAppData
152183
$Script:RfClaudeCliPath = $null
153184
Remove-RfIsolatedHome
154185
}

0 commit comments

Comments
 (0)