Quote the apiKeyHelper path, and say why the helper failed
apiKeyHelper is a shell command line, not a path, so the raw value written into settings.json was split at the first space. A Windows profile named "Mohammed Ahmed" produced an attempt to run C:\Users\Mohammed, surfacing as "your apiKeyHelper script is failing" with nothing to go on. Quote the value when it contains anything a shell cares about, and leave it bare otherwise so no existing settings.json churns on the next switch. The POSIX port had the same bug against a /Users/First Last home; shlex.quote has exactly the wanted "leave ordinary paths alone" behaviour. doctor could not see any of this. It quoted the path itself before running it, so it exercised a command line Claude Code never uses and passed while the real one failed. It now reads the string out of settings.json, reports it when it is not what a switch would write, and runs that string through a shell. The helper itself exited 1 in silence on four distinct faults - no state, no preset, no key, undecryptable key - collapsing them into one indistinguishable message. Each now names itself on stderr, which is what /status displays. The DPAPI case says what it actually means: a key stored by a different Windows account than the one Claude Code runs as. Success paths stay silent, so stdout still carries the key and nothing else. Also make install.ps1 survive a Restricted execution policy: piped through iex it is not subject to the policy, but invoking the installed script for the key prompt is, which is where a fresh install died. Set Process scope for the install, offer to set CurrentUser to RemoteSigned, and clear the mark-of-the-web that Expand-Archive can leave on the extracted scripts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+51
-9
@@ -50,6 +50,18 @@ $script:SettingsDir = Join-Path $env:USERPROFILE '.claude'
|
||||
$script:Settings = Join-Path $script:SettingsDir 'settings.json'
|
||||
$script:LmStudioDir = Join-Path $env:USERPROFILE '.lmstudio'
|
||||
|
||||
# Claude Code runs apiKeyHelper as a shell command line, not as a bare argv[0],
|
||||
# so the value in settings.json is parsed by cmd before anything is executed. A
|
||||
# profile path containing a space therefore has to arrive already quoted:
|
||||
# C:\Users\Firstname Lastname\... otherwise splits and cmd tries to run
|
||||
# C:\Users\Firstname. Paths with nothing cmd cares about are written bare,
|
||||
# exactly as before, so no existing settings.json churns on the next switch.
|
||||
function Get-HelperCommandLine {
|
||||
param([string] $Path = $script:HelperCmd)
|
||||
if ($Path -match '[\s&()^;,]') { return '"' + $Path + '"' }
|
||||
return $Path
|
||||
}
|
||||
|
||||
# Baseline set of settings.json env keys this tool owns. On every switch these
|
||||
# are deleted first, together with whatever a previous switch actually wrote
|
||||
# (tracked in state.json), so no value can survive a mode change.
|
||||
@@ -543,7 +555,7 @@ function Set-ClaudeMode {
|
||||
if (-not (Test-Path -LiteralPath $script:HelperCmd)) {
|
||||
throw "key helper missing at $($script:HelperCmd). Re-run install.ps1"
|
||||
}
|
||||
$settings['apiKeyHelper'] = $script:HelperCmd
|
||||
$settings['apiKeyHelper'] = Get-HelperCommandLine
|
||||
} else {
|
||||
$tok = 'lmstudio'
|
||||
if ($auth.Contains('token') -and $auth['token']) { $tok = [string]$auth['token'] }
|
||||
@@ -1277,7 +1289,7 @@ function Invoke-Doctor {
|
||||
Write-Head "doctor - mode '$mode'"
|
||||
|
||||
try {
|
||||
[void](Read-JsonFile $script:Settings)
|
||||
$liveSettings = Read-JsonFile $script:Settings
|
||||
Write-Ok 'settings.json parses'
|
||||
} catch {
|
||||
Write-Err2 "settings.json does not parse: $($_.Exception.Message)"
|
||||
@@ -1303,13 +1315,43 @@ function Invoke-Doctor {
|
||||
}
|
||||
else { Write-Err2 "vault '$keyRef' missing or undecryptable. Run: claude-mode set-key $keyRef" }
|
||||
|
||||
try {
|
||||
$out = (& cmd.exe /c "`"$($script:HelperCmd)`"" 2>&1 | Out-String).Trim()
|
||||
if ($out -and $key -and $out -eq $key) { Write-Ok 'apiKeyHelper emits the correct key' }
|
||||
elseif ($out) { Write-Err2 "apiKeyHelper output does not match vault key (got: $(Format-KeyMask $out))" }
|
||||
else { Write-Err2 'apiKeyHelper produced no output' }
|
||||
} catch {
|
||||
Write-Err2 "apiKeyHelper failed to run: $($_.Exception.Message)"
|
||||
# The stored string is what Claude Code hands to a shell, and a
|
||||
# path this process can quote correctly is not evidence that the
|
||||
# recorded one parses. Check the value, then run that value.
|
||||
$stored = ''
|
||||
if ($liveSettings.Contains('apiKeyHelper')) { $stored = [string]$liveSettings['apiKeyHelper'] }
|
||||
$expected = Get-HelperCommandLine
|
||||
$reSwitch = "claude-mode $mode $([string]$state['preset'])"
|
||||
|
||||
if (-not $stored) {
|
||||
Write-Err2 "settings.json has no apiKeyHelper. Run: $reSwitch"
|
||||
} elseif ($stored -ne $expected) {
|
||||
Write-Err2 "apiKeyHelper reads $stored"
|
||||
Write-Err2 " but should read $expected - run: $reSwitch"
|
||||
} else {
|
||||
Write-Ok "apiKeyHelper wired as $stored"
|
||||
}
|
||||
|
||||
if ($stored) {
|
||||
# Run it through cmd the way a shell would, via a batch file, so
|
||||
# PowerShell's own native-argument quoting cannot paper over a
|
||||
# value that a real shell would split.
|
||||
$probe = Join-Path $env:TEMP ('cm-helper-probe-' + [IO.Path]::GetRandomFileName().Replace('.', '') + '.cmd')
|
||||
try {
|
||||
Set-Content -LiteralPath $probe -Value ("@echo off`r`n" + $stored) -Encoding ASCII
|
||||
$out = (& cmd.exe /c "`"$probe`"" 2>&1 | Out-String).Trim()
|
||||
if ($out -and $key -and $out -eq $key) { Write-Ok 'apiKeyHelper emits the correct key' }
|
||||
elseif ($out -match '^\S+$') {
|
||||
# One unbroken token: a credential, just the wrong one. Never echo it.
|
||||
Write-Err2 "apiKeyHelper output does not match vault key (got: $(Format-KeyMask $out))"
|
||||
}
|
||||
elseif ($out) { Write-Err2 "apiKeyHelper failed: $out" }
|
||||
else { Write-Err2 'apiKeyHelper produced no output' }
|
||||
} catch {
|
||||
Write-Err2 "apiKeyHelper failed to run: $($_.Exception.Message)"
|
||||
} finally {
|
||||
Remove-Item -LiteralPath $probe -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
if ($key -and $mode -eq 'openrouter') {
|
||||
|
||||
Reference in New Issue
Block a user