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:
@@ -92,6 +92,37 @@ nothing when state says `anthropic` — belt and braces. LM Studio's `lmstudio`
|
|||||||
token is a placeholder, not a secret, so it's written inline and the helper stays
|
token is a placeholder, not a secret, so it's written inline and the helper stays
|
||||||
out of it.
|
out of it.
|
||||||
|
|
||||||
|
### When the helper "is failing"
|
||||||
|
|
||||||
|
Claude Code reports a broken helper as *your apiKeyHelper script is failing* and
|
||||||
|
shows its stderr under `/status`. Two causes account for nearly all of it.
|
||||||
|
|
||||||
|
**A space in your home directory.** `apiKeyHelper` is a shell *command line*, not
|
||||||
|
a path, so `C:\Users\Firstname Lastname\.claude-mode\bin\claude-key-helper.cmd`
|
||||||
|
is split at the space and cmd tries to run `C:\Users\Firstname`. The value is now
|
||||||
|
quoted when it needs to be, on both ports — `shlex.quote` on POSIX, where a
|
||||||
|
`/Users/Firstname Lastname` home does the same thing. Paths that need no quoting
|
||||||
|
are written bare exactly as before, so no existing `settings.json` churns.
|
||||||
|
|
||||||
|
`doctor` used to miss this, because it quoted the path itself before running it
|
||||||
|
and so tested something Claude Code never sees. It now reads the string out of
|
||||||
|
`settings.json`, says so when that string is not what a switch would write, and
|
||||||
|
executes *that* string through a shell. Re-running the switch rewrites it:
|
||||||
|
|
||||||
|
```
|
||||||
|
claude-mode openrouter default
|
||||||
|
claude-mode doctor
|
||||||
|
```
|
||||||
|
|
||||||
|
**A key stored by a different Windows account.** The vault is DPAPI `CurrentUser`
|
||||||
|
scope, so a key stored from an elevated or *run as* shell cannot be decrypted by
|
||||||
|
the account Claude Code runs as. Re-run `claude-mode set-key <ref>` unelevated,
|
||||||
|
as yourself.
|
||||||
|
|
||||||
|
Every helper failure path now names itself on stderr rather than exiting 1 in
|
||||||
|
silence, so `/status` distinguishes these from a missing preset or an empty
|
||||||
|
vault. The success paths stay silent — stdout carries the key and nothing else.
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -13,15 +13,31 @@
|
|||||||
|
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
# Claude Code surfaces a failing helper as "your apiKeyHelper script is failing"
|
||||||
|
# and shows this stream under /status. Exiting 1 in silence turns four different
|
||||||
|
# faults into one indistinguishable message, so every failure path says which it
|
||||||
|
# was. Success paths stay silent - stdout carries the key and nothing else.
|
||||||
|
function Fail {
|
||||||
|
param([string] $Message)
|
||||||
|
[Console]::Error.WriteLine("claude-key-helper: $Message")
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$root = Join-Path $env:USERPROFILE '.claude-mode'
|
$root = Join-Path $env:USERPROFILE '.claude-mode'
|
||||||
$state = Get-Content -LiteralPath (Join-Path $root 'state.json') -Raw -Encoding UTF8 | ConvertFrom-Json
|
$statePath = Join-Path $root 'state.json'
|
||||||
|
if (-not (Test-Path -LiteralPath $statePath)) {
|
||||||
|
Fail "no state.json at $statePath - claude-mode is not installed for this Windows account. Run install.ps1"
|
||||||
|
}
|
||||||
|
$state = Get-Content -LiteralPath $statePath -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
|
||||||
if ($state.mode -eq 'anthropic') { exit 0 }
|
if ($state.mode -eq 'anthropic') { exit 0 }
|
||||||
if (-not $state.preset) { exit 0 }
|
if (-not $state.preset) { exit 0 }
|
||||||
|
|
||||||
$presetPath = Join-Path $root ('presets\' + $state.preset + '.json')
|
$presetPath = Join-Path $root ('presets\' + $state.preset + '.json')
|
||||||
if (-not (Test-Path -LiteralPath $presetPath)) { exit 1 }
|
if (-not (Test-Path -LiteralPath $presetPath)) {
|
||||||
|
Fail "state.json names preset '$($state.preset)' but $presetPath does not exist. Run: claude-mode presets"
|
||||||
|
}
|
||||||
$preset = Get-Content -LiteralPath $presetPath -Raw -Encoding UTF8 | ConvertFrom-Json
|
$preset = Get-Content -LiteralPath $presetPath -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
|
||||||
$authMode = 'vault'
|
$authMode = 'vault'
|
||||||
@@ -34,15 +50,24 @@ try {
|
|||||||
if ($authMode -ne 'vault') { exit 0 } # inline token: nothing for us to emit
|
if ($authMode -ne 'vault') { exit 0 } # inline token: nothing for us to emit
|
||||||
|
|
||||||
$credPath = Join-Path $root ('vault\' + $keyRef + '.cred')
|
$credPath = Join-Path $root ('vault\' + $keyRef + '.cred')
|
||||||
if (-not (Test-Path -LiteralPath $credPath)) { exit 1 }
|
if (-not (Test-Path -LiteralPath $credPath)) {
|
||||||
|
Fail "no key stored for ref '$keyRef'. Run: claude-mode set-key $keyRef"
|
||||||
|
}
|
||||||
|
|
||||||
$secure = ConvertTo-SecureString (Get-Content -LiteralPath $credPath -Raw).Trim()
|
try {
|
||||||
$bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure)
|
$secure = ConvertTo-SecureString (Get-Content -LiteralPath $credPath -Raw).Trim()
|
||||||
|
} catch {
|
||||||
|
# DPAPI is CurrentUser scope, so this is almost always a key stored by a
|
||||||
|
# different Windows account than the one Claude Code is running as -
|
||||||
|
# typically set-key run from an elevated or "run as" shell.
|
||||||
|
Fail "cannot decrypt $credPath as $env:USERDOMAIN\$env:USERNAME. The key is DPAPI-encrypted for whichever account stored it; re-run 'claude-mode set-key $keyRef' as this user, unelevated."
|
||||||
|
}
|
||||||
|
$bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure)
|
||||||
try {
|
try {
|
||||||
[Console]::Out.Write([Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr))
|
[Console]::Out.Write([Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr))
|
||||||
} finally {
|
} finally {
|
||||||
[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
|
[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
exit 1
|
Fail $_.Exception.Message
|
||||||
}
|
}
|
||||||
|
|||||||
+51
-9
@@ -50,6 +50,18 @@ $script:SettingsDir = Join-Path $env:USERPROFILE '.claude'
|
|||||||
$script:Settings = Join-Path $script:SettingsDir 'settings.json'
|
$script:Settings = Join-Path $script:SettingsDir 'settings.json'
|
||||||
$script:LmStudioDir = Join-Path $env:USERPROFILE '.lmstudio'
|
$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
|
# 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
|
# are deleted first, together with whatever a previous switch actually wrote
|
||||||
# (tracked in state.json), so no value can survive a mode change.
|
# (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)) {
|
if (-not (Test-Path -LiteralPath $script:HelperCmd)) {
|
||||||
throw "key helper missing at $($script:HelperCmd). Re-run install.ps1"
|
throw "key helper missing at $($script:HelperCmd). Re-run install.ps1"
|
||||||
}
|
}
|
||||||
$settings['apiKeyHelper'] = $script:HelperCmd
|
$settings['apiKeyHelper'] = Get-HelperCommandLine
|
||||||
} else {
|
} else {
|
||||||
$tok = 'lmstudio'
|
$tok = 'lmstudio'
|
||||||
if ($auth.Contains('token') -and $auth['token']) { $tok = [string]$auth['token'] }
|
if ($auth.Contains('token') -and $auth['token']) { $tok = [string]$auth['token'] }
|
||||||
@@ -1277,7 +1289,7 @@ function Invoke-Doctor {
|
|||||||
Write-Head "doctor - mode '$mode'"
|
Write-Head "doctor - mode '$mode'"
|
||||||
|
|
||||||
try {
|
try {
|
||||||
[void](Read-JsonFile $script:Settings)
|
$liveSettings = Read-JsonFile $script:Settings
|
||||||
Write-Ok 'settings.json parses'
|
Write-Ok 'settings.json parses'
|
||||||
} catch {
|
} catch {
|
||||||
Write-Err2 "settings.json does not parse: $($_.Exception.Message)"
|
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" }
|
else { Write-Err2 "vault '$keyRef' missing or undecryptable. Run: claude-mode set-key $keyRef" }
|
||||||
|
|
||||||
try {
|
# The stored string is what Claude Code hands to a shell, and a
|
||||||
$out = (& cmd.exe /c "`"$($script:HelperCmd)`"" 2>&1 | Out-String).Trim()
|
# path this process can quote correctly is not evidence that the
|
||||||
if ($out -and $key -and $out -eq $key) { Write-Ok 'apiKeyHelper emits the correct key' }
|
# recorded one parses. Check the value, then run that value.
|
||||||
elseif ($out) { Write-Err2 "apiKeyHelper output does not match vault key (got: $(Format-KeyMask $out))" }
|
$stored = ''
|
||||||
else { Write-Err2 'apiKeyHelper produced no output' }
|
if ($liveSettings.Contains('apiKeyHelper')) { $stored = [string]$liveSettings['apiKeyHelper'] }
|
||||||
} catch {
|
$expected = Get-HelperCommandLine
|
||||||
Write-Err2 "apiKeyHelper failed to run: $($_.Exception.Message)"
|
$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') {
|
if ($key -and $mode -eq 'openrouter') {
|
||||||
|
|||||||
+33
-3
@@ -18,6 +18,12 @@ param(
|
|||||||
Set-StrictMode -Version 1.0
|
Set-StrictMode -Version 1.0
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
# Piped in as `irm ... | iex` the installer itself is never subject to the
|
||||||
|
# execution policy - but invoking the installed claude-mode.ps1 below is, and
|
||||||
|
# on a stock Restricted machine that fails. Process scope lasts only for this
|
||||||
|
# powershell.exe and does not weaken the machine or user policy.
|
||||||
|
try { Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force -ErrorAction Stop } catch { }
|
||||||
|
|
||||||
$repoUrl = 'https://git.nebulm.com/smoido/claude-mode'
|
$repoUrl = 'https://git.nebulm.com/smoido/claude-mode'
|
||||||
|
|
||||||
# Running from a checkout, the source is the files next to this script. Piped
|
# Running from a checkout, the source is the files next to this script. Piped
|
||||||
@@ -144,13 +150,37 @@ if ($current -match [regex]::Escape($startMark)) {
|
|||||||
Write-Host " ok appended claude-mode block to $profilePath" -ForegroundColor Green
|
Write-Host " ok appended claude-mode block to $profilePath" -ForegroundColor Green
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- 7. execution policy check ---------------------------------------------
|
# --- 7. execution policy ----------------------------------------------------
|
||||||
|
# Without this the profile block above never loads, so `claude-mode` and the
|
||||||
|
# `claude` wrapper simply do not exist in PowerShell. Offer to fix it rather
|
||||||
|
# than printing a warning the user has to act on later.
|
||||||
$pol = Get-ExecutionPolicy -Scope CurrentUser
|
$pol = Get-ExecutionPolicy -Scope CurrentUser
|
||||||
if ($pol -in @('Restricted', 'Undefined', 'AllSigned')) {
|
if ($pol -in @('Restricted', 'Undefined', 'AllSigned')) {
|
||||||
Write-Host " warn CurrentUser execution policy is '$pol'; the profile will not load." -ForegroundColor Yellow
|
$effective = Get-ExecutionPolicy
|
||||||
Write-Host " Fix: Set-ExecutionPolicy -Scope CurrentUser RemoteSigned" -ForegroundColor Yellow
|
if ($effective -in @('Restricted', 'AllSigned')) {
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host " warn execution policy is '$effective'; the profile block will not load," -ForegroundColor Yellow
|
||||||
|
Write-Host " so 'claude-mode' will not be a command in PowerShell." -ForegroundColor Yellow
|
||||||
|
$ans = Read-Host ' Set CurrentUser policy to RemoteSigned now? [Y/n]'
|
||||||
|
if ($ans -eq '' -or $ans -match '^(y|yes)$') {
|
||||||
|
try {
|
||||||
|
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned -Force -ErrorAction Stop
|
||||||
|
Write-Host ' ok CurrentUser execution policy set to RemoteSigned' -ForegroundColor Green
|
||||||
|
} catch {
|
||||||
|
Write-Host " warn could not set policy: $($_.Exception.Message)" -ForegroundColor Yellow
|
||||||
|
Write-Host ' Run manually: Set-ExecutionPolicy -Scope CurrentUser RemoteSigned' -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host ' Skipped. Run manually: Set-ExecutionPolicy -Scope CurrentUser RemoteSigned' -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Expand-Archive can carry the mark-of-the-web onto extracted files, which
|
||||||
|
# RemoteSigned then refuses. Clear it on what was just installed.
|
||||||
|
Get-ChildItem -LiteralPath $root -Recurse -Filter '*.ps1' -ErrorAction SilentlyContinue |
|
||||||
|
Unblock-File -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
# --- 8. key ----------------------------------------------------------------
|
# --- 8. key ----------------------------------------------------------------
|
||||||
if (-not $SkipKeyPrompt) {
|
if (-not $SkipKeyPrompt) {
|
||||||
$vault = Join-Path $root 'vault\openrouter.cred'
|
$vault = Join-Path $root 'vault\openrouter.cred'
|
||||||
|
|||||||
@@ -8,6 +8,12 @@
|
|||||||
|
|
||||||
set -u
|
set -u
|
||||||
|
|
||||||
|
# Claude Code surfaces a failing helper as "your apiKeyHelper script is failing"
|
||||||
|
# and shows this stream under /status. Exiting 1 in silence turns several
|
||||||
|
# distinct faults into one indistinguishable message, so every failure path says
|
||||||
|
# which it was. Success paths stay silent - stdout carries the key and nothing else.
|
||||||
|
fail() { printf 'claude-key-helper: %s\n' "$*" >&2; exit 1; }
|
||||||
|
|
||||||
CM_ROOT="${CM_ROOT:-$HOME/.claude-mode}"
|
CM_ROOT="${CM_ROOT:-$HOME/.claude-mode}"
|
||||||
# shellcheck source=/dev/null
|
# shellcheck source=/dev/null
|
||||||
. "$CM_ROOT/bin/cm-vault.sh"
|
. "$CM_ROOT/bin/cm-vault.sh"
|
||||||
@@ -26,7 +32,7 @@ preset_name="$("$PY" "$JSON" get "$state" preset 2>/dev/null)"
|
|||||||
[ -n "$preset_name" ] || exit 0
|
[ -n "$preset_name" ] || exit 0
|
||||||
|
|
||||||
preset="$CM_ROOT/presets/$preset_name.json"
|
preset="$CM_ROOT/presets/$preset_name.json"
|
||||||
[ -f "$preset" ] || exit 1
|
[ -f "$preset" ] || fail "state.json names preset '$preset_name' but $preset does not exist. Run: claude-mode presets"
|
||||||
|
|
||||||
auth_mode="$("$PY" "$JSON" get "$preset" auth.mode 2>/dev/null)"
|
auth_mode="$("$PY" "$JSON" get "$preset" auth.mode 2>/dev/null)"
|
||||||
[ -z "$auth_mode" ] && auth_mode="vault"
|
[ -z "$auth_mode" ] && auth_mode="vault"
|
||||||
@@ -35,4 +41,4 @@ auth_mode="$("$PY" "$JSON" get "$preset" auth.mode 2>/dev/null)"
|
|||||||
key_ref="$("$PY" "$JSON" get "$preset" auth.keyRef 2>/dev/null)"
|
key_ref="$("$PY" "$JSON" get "$preset" auth.keyRef 2>/dev/null)"
|
||||||
[ -n "$key_ref" ] || key_ref="openrouter"
|
[ -n "$key_ref" ] || key_ref="openrouter"
|
||||||
|
|
||||||
cm_vault_get "$key_ref" || exit 1
|
cm_vault_get "$key_ref" || fail "no key readable for ref '$key_ref' from $(cm_vault_backend_label 2>/dev/null || echo 'the vault'). Run: claude-mode set-key $key_ref"
|
||||||
|
|||||||
+26
-4
@@ -1283,10 +1283,32 @@ cmd_doctor() {
|
|||||||
else
|
else
|
||||||
err "vault '$ref' missing. Run: claude-mode set-key $ref"; key=''
|
err "vault '$ref' missing. Run: claude-mode set-key $ref"; key=''
|
||||||
fi
|
fi
|
||||||
out="$("$CM_HELPER" 2>/dev/null)"
|
# Check the string settings.json actually holds, then run that
|
||||||
if [ -n "$out" ] && [ "$out" = "$key" ]; then ok 'apiKeyHelper emits the correct key'
|
# string through a shell. A path this script can quote correctly is
|
||||||
elif [ -n "$out" ]; then err 'apiKeyHelper output does not match the vault'
|
# no evidence that the recorded one parses.
|
||||||
else err 'apiKeyHelper produced no output'; fi
|
local stored expected reswitch
|
||||||
|
stored="$(jget "$CM_SETTINGS" apiKeyHelper)"
|
||||||
|
expected="$("$PY" -c 'import shlex,sys; sys.stdout.write(shlex.quote(sys.argv[1]))' "$CM_HELPER")"
|
||||||
|
reswitch="claude-mode $mode $(jget "$CM_STATE" preset)"
|
||||||
|
if [ -z "$stored" ]; then
|
||||||
|
err "settings.json has no apiKeyHelper. Run: $reswitch"
|
||||||
|
elif [ "$stored" != "$expected" ]; then
|
||||||
|
err "apiKeyHelper reads $stored"
|
||||||
|
err " but should read $expected - run: $reswitch"
|
||||||
|
else
|
||||||
|
ok "apiKeyHelper wired as $stored"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$stored" ]; then
|
||||||
|
out="$(sh -c "$stored" 2>&1)"
|
||||||
|
if [ -n "$out" ] && [ "$out" = "$key" ]; then ok 'apiKeyHelper emits the correct key'
|
||||||
|
elif printf '%s' "$out" | grep -q '[[:space:]]'; then
|
||||||
|
# Whitespace means a diagnostic, not a credential; a key is
|
||||||
|
# one unbroken token and must never be echoed.
|
||||||
|
err "apiKeyHelper failed: $out"
|
||||||
|
elif [ -n "$out" ]; then err "apiKeyHelper output does not match the vault (got: $(cm_vault_mask "$out"))"
|
||||||
|
else err 'apiKeyHelper produced no output'; fi
|
||||||
|
fi
|
||||||
|
|
||||||
if [ -n "$key" ] && [ "$mode" = "openrouter" ]; then
|
if [ -n "$key" ] && [ "$mode" = "openrouter" ]; then
|
||||||
local kinfo
|
local kinfo
|
||||||
|
|||||||
+6
-1
@@ -20,6 +20,7 @@ import glob
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import shlex
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
# Matches both the qualified gateway id (anthropic/claude-opus-5) and the bare
|
# Matches both the qualified gateway id (anthropic/claude-opus-5) and the bare
|
||||||
@@ -156,7 +157,11 @@ def cmd_apply(argv):
|
|||||||
if auth.get("mode") == "vault":
|
if auth.get("mode") == "vault":
|
||||||
if not helper:
|
if not helper:
|
||||||
raise SystemExit("apply: vault auth needs the key-helper path")
|
raise SystemExit("apply: vault auth needs the key-helper path")
|
||||||
settings["apiKeyHelper"] = helper
|
# Claude Code runs this value as a shell command line, so a $HOME
|
||||||
|
# containing a space (common on macOS) has to arrive quoted or the
|
||||||
|
# shell splits it and tries to execute the first word. shlex.quote
|
||||||
|
# leaves an ordinary path untouched, so nothing churns.
|
||||||
|
settings["apiKeyHelper"] = shlex.quote(helper)
|
||||||
else:
|
else:
|
||||||
block["ANTHROPIC_AUTH_TOKEN"] = str(auth.get("token") or "lmstudio")
|
block["ANTHROPIC_AUTH_TOKEN"] = str(auth.get("token") or "lmstudio")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user