set-key takes the ref it was given, and a key inline

`claude-mode set-key openrouter sk-or-...` did the wrong thing on the
POSIX port: the dispatcher loop kept the *last* positional, so the key
became the ref name and the prompt asked for "the key for ref
'sk-or-...'". Storing to that ref succeeds, so nothing looks wrong until
the switch later reports no key for 'openrouter' - which is exactly what
happened on macOS.

Both ports now take `set-key [ref] [key]`: the first positional is the
ref unless it is key-shaped (sk-* or longer than a ref name would
plausibly be), in which case it is the key for the default ref, and a
second positional is always the key. An inline key warns that it is now
in the shell history, since the hidden prompt leaves no trace. Extra
arguments are refused instead of ignored. The Windows dispatch already
read the first positional correctly, so it only gains the inline form.

The shipped default preset moves to the models asked for: DeepSeek 4.1
Flash on opus, GLM Flash on sonnet. Existing installs keep their own
presets - the installer never overwrites one without --force - so an
upgrade needs `claude-mode preset set default opus ...` and the same for
sonnet, or a --force install.

Verified in the bash 3.2 container: ref+key stores under the ref, a bare
key stores under openrouter, an empty prompt aborts, a custom ref still
works, the stored value is byte-identical, and four positionals are
refused.

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
smoido
2026-09-12 23:26:19 +03:00
co-authored by Claude Code
parent d86a2ffe77
commit 5bf5a6d36e
5 changed files with 2270 additions and 2227 deletions
+2 -2
View File
@@ -186,7 +186,7 @@ claude-mode preset set <name> <tier> <model-id>
claude-mode preset all <name> <model-id> point every tier at one model
claude-mode preset rm <name>
claude-mode set-key [ref] store a key (hidden prompt, DPAPI)
claude-mode set-key [ref] [key] store a key (hidden prompt; [key] for scripts)
claude-mode models [filter] models available from the active provider
claude-mode doctor verify auth, endpoint, model ids, stray env vars
@@ -209,7 +209,7 @@ menu to read before the thing you asked for happens. Build more with
| preset | provider | opus | sonnet | haiku | fable |
|---|---|---|---|---|---|
| `default` | openrouter | `z-ai/glm-5.3-flash` | `deepseek/deepseek-v4-flash-0731` | `openrouter/free` | `z-ai/glm-5.3` |
| `default` | openrouter | `deepseek/deepseek-v4.1-flash` | `z-ai/glm-5.3-flash` | `openrouter/free` | `z-ai/glm-5.3` |
| `zai` | zai | `glm-5.3` | `glm-5.3` | `glm-4.7` | `glm-5.3` |
| `lmstudio` | lmstudio | whatever setup finds on your server (all tiers) | | | |
+1 -1
View File
@@ -1 +1 @@
1.9.2
1.9.3
+22 -4
View File
@@ -181,7 +181,7 @@ claude-mode - switch Claude Code between Anthropic, OpenRouter, Z.AI, LM Studio
point every tier at one model
claude-mode preset rm <name>
claude-mode set-key [ref] store an API key (hidden prompt, DPAPI-encrypted)
claude-mode set-key [ref] [key] store an API key (hidden prompt, DPAPI-encrypted)
claude-mode models [filter] models available from the active provider
claude-mode doctor verify auth, endpoint, model ids, stray env vars
claude-mode repair [--all] strip [1m] tags from cached model ids
@@ -375,11 +375,16 @@ function ConvertFrom-SecureStringPlain {
}
function Set-VaultKey {
param([string] $Ref)
param([string] $Ref, [string] $Key = '')
Initialize-Root
if ($Key) {
Write-Warn2 'the key was given on the command line, so it is in this shell history - the hidden prompt leaves no trace'
$plain = $Key
} else {
Write-Host "Paste the API key for ref '$Ref' (input hidden):"
$secure = Read-Host -AsSecureString
$plain = ConvertFrom-SecureStringPlain $secure
}
if ([string]::IsNullOrWhiteSpace($plain)) { throw 'empty key, aborted' }
# A hidden prompt will happily swallow a mis-paste. Guard the two shapes that
@@ -392,7 +397,8 @@ function Set-VaultKey {
throw "that value is a claude-mode command, not an API key. Nothing was stored."
}
if ($plain.Length -lt 16) {
Write-Warn2 "that key is only $($plain.Length) characters - unusually short. Storing anyway."
$unit = if ($plain.Length -eq 1) { 'character' } else { 'characters' }
Write-Warn2 "that key is only $($plain.Length) $unit - unusually short. Storing anyway."
}
if ($Ref -eq 'openrouter' -and $plain -notlike 'sk-or-*') {
Write-Warn2 "key does not start with 'sk-or-' - storing anyway"
@@ -2198,7 +2204,19 @@ try {
}
'presets' { Invoke-Presets }
'preset' { Invoke-PresetCmd -Argv $Rest }
'set-key' { Set-VaultKey -Ref $(if ($Rest -and $Rest.Count -ge 1) { $Rest[0] } else { 'openrouter' }) }
'set-key' {
# `set-key [ref] [key]`. The key is normally typed at the hidden
# prompt; anything key-shaped in the ref position is taken as the
# key for the default ref, which is the easy mistake to make.
$kRef = ''; $kKey = ''
foreach ($a in $Rest) {
if (-not $kRef -and -not ($a -like 'sk-*' -or $a.Length -gt 24)) { $kRef = $a; continue }
if (-not $kKey) { $kKey = $a; continue }
throw "unexpected extra argument '$a'"
}
if (-not $kRef) { $kRef = 'openrouter' }
Set-VaultKey -Ref $kRef -Key $kKey
}
'models' { Invoke-Models -Filter $(if ($Rest -and $Rest.Count -ge 1) { $Rest[0] } else { $null }) }
'doctor' { Invoke-Doctor }
'repair' { [void](Invoke-Repair -All:([bool]($Rest -and ($Rest -contains '--all')))) }
+31 -6
View File
@@ -269,7 +269,7 @@ claude-mode - switch Claude Code between Anthropic, OpenRouter, Z.AI, LM Studio
claude-mode preset auth <name> none|key [ref] whether that server needs a key
claude-mode preset rm <name>
claude-mode set-key [ref] [--terminal] store an API key (hidden prompt)
claude-mode set-key [ref] [key] store an API key (hidden prompt; key for scripts)
claude-mode models [filter] models available from the active provider
claude-mode doctor verify auth, endpoint, model ids, env
claude-mode repair strip [1m] tags from cached model ids
@@ -1242,15 +1242,20 @@ cmd_models() {
}
cmd_set_key() {
local ref="${1:-openrouter}" secret
local ref="${1:-openrouter}" inline="${2:-}" secret unit
init_root
printf ' storage backend: %s\n' "$(cm_vault_backend_label)"
if [ "$(cm_vault_backend)" = "file" ]; then
warn 'no keyring available - the key will be stored in a 0600 file, NOT encrypted.'
warn 'install libsecret-tools (secret-tool) or pass for encrypted storage.'
fi
if [ -n "$inline" ]; then
secret="$inline"
warn 'the key was given on the command line, so it is in this shell history - the hidden prompt leaves no trace'
else
printf ' paste the API key for ref '\''%s'\'' (input hidden): ' "$ref"
IFS= read -rs secret; printf '\n'
fi
[ -n "$secret" ] || { err 'empty key, aborted'; return 1; }
# A hidden prompt will happily swallow a mis-paste. Guard the two shapes that
@@ -1260,7 +1265,10 @@ cmd_set_key() {
*[[:space:]]*) err 'that value contains whitespace, so it is not an API key (a pasted command line?). Nothing was stored.'; return 1 ;;
claude-mode*) err 'that value is a claude-mode command, not an API key. Nothing was stored.'; return 1 ;;
esac
[ "${#secret}" -lt 16 ] && warn "that key is only ${#secret} characters - unusually short. Storing anyway."
if [ "${#secret}" -lt 16 ]; then
unit=characters; [ "${#secret}" -eq 1 ] && unit=character
warn "that key is only ${#secret} $unit - unusually short. Storing anyway."
fi
if [ "$ref" = "openrouter" ] && [ "${secret#sk-or-}" = "$secret" ]; then
warn "key does not start with 'sk-or-' - storing anyway"
fi
@@ -2432,13 +2440,30 @@ case "$cmd" in
# --terminal is for callers with no stdin to offer: the bar
# widget cannot host a hidden password prompt, so it asks the
# terminal to host one instead.
_ref=openrouter; _term=0
# `set-key [ref] [key]`. The key is normally typed at the
# hidden prompt; a second word is taken as the key itself, for
# scripting. Anything key-shaped in the *ref* position is also
# taken as the key for the default ref - it used to become the
# ref name instead, which stored the wrong thing under a ref
# nobody would ever look up, and only failed later and quietly.
_ref=''; _term=0; _secret=''
for _a in "$@"; do
case "$_a" in
--terminal) _term=1 ;;
*) _ref="$_a" ;;
*)
if [ -z "$_ref" ]; then
case "$_a" in
sk-*|sk_*) _secret="$_a" ;;
*) if [ "${#_a}" -gt 24 ]; then _secret="$_a"; else _ref="$_a"; fi ;;
esac
elif [ -z "$_secret" ]; then
_secret="$_a"
else
err "unexpected extra argument '$_a'"; exit 1
fi ;;
esac
done
[ -n "$_ref" ] || _ref=openrouter
if [ "$_term" -eq 1 ]; then
_t="$(cm_terminal_cmd)"
setsid nohup "$_t" -e bash -lc \
@@ -2446,7 +2471,7 @@ case "$cmd" in
>/dev/null 2>&1 &
ok "opened $_t to store the '$_ref' key"
else
cmd_set_key "$_ref"
cmd_set_key "$_ref" "$_secret"
fi ;;
models) cmd_models "${1:-}" ;;
doctor) cmd_doctor ;;
+3 -3
View File
@@ -1,14 +1,14 @@
{
"provider": "openrouter",
"description": "Daily driver. Flash-class models on the hot tiers, GLM on fable.",
"description": "Daily driver. DeepSeek 4.1 Flash on opus, GLM Flash on sonnet, GLM on fable.",
"baseUrl": "https://openrouter.ai/api",
"auth": {
"mode": "vault",
"keyRef": "openrouter"
},
"models": {
"opus": "z-ai/glm-5.3-flash",
"sonnet": "deepseek/deepseek-v4-flash-0731",
"opus": "deepseek/deepseek-v4.1-flash",
"sonnet": "z-ai/glm-5.3-flash",
"haiku": "openrouter/free",
"fable": "z-ai/glm-5.3"
},