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:
@@ -8,6 +8,12 @@
|
||||
|
||||
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}"
|
||||
# shellcheck source=/dev/null
|
||||
. "$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
|
||||
|
||||
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)"
|
||||
[ -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)"
|
||||
[ -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
|
||||
err "vault '$ref' missing. Run: claude-mode set-key $ref"; key=''
|
||||
fi
|
||||
out="$("$CM_HELPER" 2>/dev/null)"
|
||||
if [ -n "$out" ] && [ "$out" = "$key" ]; then ok 'apiKeyHelper emits the correct key'
|
||||
elif [ -n "$out" ]; then err 'apiKeyHelper output does not match the vault'
|
||||
else err 'apiKeyHelper produced no output'; fi
|
||||
# Check the string settings.json actually holds, then run that
|
||||
# string through a shell. A path this script can quote correctly is
|
||||
# no evidence that the recorded one parses.
|
||||
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
|
||||
local kinfo
|
||||
|
||||
+6
-1
@@ -20,6 +20,7 @@ import glob
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import sys
|
||||
|
||||
# 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 not helper:
|
||||
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:
|
||||
block["ANTHROPIC_AUTH_TOKEN"] = str(auth.get("token") or "lmstudio")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user