Each provider's preset list ends in "New ... preset...", and the preset editor gains Duplicate, Rename and Delete. New asks for a name and what to start from: a copy of one of that provider's presets, or a blank template. Delete is greyed out, with the reason, on the preset in use, and warns when it would leave a provider with no preset at all. CLI: - preset rename <name> <new>: the new name is hard-linked in, state.json is repointed, and only then does the old name go. - preset new <name> --provider <p> [--blank]: with a provider and no source it copies that provider's own default instead of the OpenRouter-only `default`; --blank starts from the scaffold. - valid_preset_name on every preset subcommand: no slash, no leading dot. `preset show ../../etc/passwd` used to print the file. - preset rm warns when it removes a provider's last preset. - preflight refuses a preset with every tier empty. Otherwise a blank preset would switch cleanly and Claude Code would ask the gateway for its default Anthropic models, billed at full price on OpenRouter. The panel's blocked card offers "Edit preset..." for it. The key helper now reads the active preset in a single open (new cm-json auth-of) and re-reads state.json once on a miss. Renaming the active preset could otherwise catch a running session between reading the old name and opening the file: 1 failure in 51 key fetches in a race test before, 0 in 118 across 60 renames after. It could also briefly hand out the openrouter key for a preset that uses another. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
52 lines
2.2 KiB
Bash
52 lines
2.2 KiB
Bash
#!/usr/bin/env bash
|
|
# Invoked by Claude Code via the `apiKeyHelper` setting. Prints the API key for
|
|
# the active preset to stdout and nothing else.
|
|
#
|
|
# Only presets whose auth mode is "vault" have a secret to emit. In anthropic
|
|
# mode, or for a preset using an inline placeholder token (LM Studio), this exits
|
|
# silently so a token cannot leak into a context that must not have one.
|
|
|
|
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"
|
|
|
|
PY="${CLAUDE_MODE_PYTHON:-python3}"
|
|
JSON="$CM_ROOT/bin/cm-json.py"
|
|
|
|
state="$CM_ROOT/state.json"
|
|
[ -f "$state" ] || exit 0
|
|
|
|
mode="$("$PY" "$JSON" get "$state" mode 2>/dev/null)"
|
|
[ "$mode" = "anthropic" ] && exit 0
|
|
[ -n "$mode" ] || exit 0
|
|
|
|
# `preset rename` on the active preset moves the file while this may be running
|
|
# in any live session. The name read from state.json can be the old one by the
|
|
# time the file is opened, so the preset is read in a single open (a removal
|
|
# after that cannot change what was read), and a miss re-reads state.json once:
|
|
# the rename repoints it before removing the old name, so the second read finds
|
|
# the new one. A second miss is a real fault.
|
|
auth=''
|
|
for _attempt in 1 2; do
|
|
preset_name="$("$PY" "$JSON" get "$state" preset 2>/dev/null)"
|
|
[ -n "$preset_name" ] || exit 0
|
|
preset="$CM_ROOT/presets/$preset_name.json"
|
|
auth="$("$PY" "$JSON" auth-of "$preset" 2>/dev/null)" && break
|
|
auth=''
|
|
done
|
|
[ -n "$auth" ] || fail "state.json names preset '$preset_name' but $preset does not exist. Run: claude-mode presets"
|
|
|
|
auth_mode="${auth%%$'\t'*}"
|
|
key_ref="${auth#*$'\t'}"
|
|
[ "$auth_mode" = "vault" ] || exit 0 # inline token: nothing for us to emit
|
|
|
|
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"
|