Two findings from the review of 5fc391a4, both fixed here rather than sent back.
**The API-key trust seed never matched a real key.** `seedApiKeyTrustFile()` wrote the
key verbatim into `customApiKeyResponses.approved`, but Claude Code stores and compares
only the last 20 characters (`key.trim().slice(-20)`, applied on both the write and the
lookup). For any real key the seed missed, so claude stopped at the interactive
"Detected a custom API key in your environment" prompt, whose default is
"No (recommended)": the launch hangs, or silently refuses the key this feature just
injected and falls through to an OAuth login the isolated config dir does not have. It
survived review because a keyless llama.cpp/llama-swap endpoint uses DEFAULT_API_KEY
('local-dummy-key', 15 chars), where slice(-20) returns the whole string and the seed
matches by accident, and every test used a key shorter than that. Now truncated through
`truncateApiKeyForTrustFile()`, with a test using a 57-character key that also asserts
the full credential never reaches that second file.
**One `confirmed` flag answered two different questions.** The context-floor warning
("this model's window is below what this CLI needs") and the swap-conflict warning
("loading this unloads the model another session is using") shared it, and the context
check runs first, so a user clicking "launch anyway" past the context warning silently
consented to evicting someone else's model. They are about different people, so an
answer to one is not consent to the other. Both routes now read `confirmedContext` and
`confirmedSwap` independently; the legacy `confirmed` still means both, because it
shipped in this feature's HTTP-API-only cut and an existing caller must keep working.
The frontend answers each question with its own flag and accumulates them, on the
one-shot path, the restart path and the batch carry-forward alike.
Also from the same review: the swap-confirm dialog no longer renders " are currently
using ..." when multi-user scoping leaves the affected-session list empty (the swap is
blocked regardless of ownership; only the NAMES are scoped), and the per-endpoint
llama-swap log tails are closed in `WebServer.stop()` instead of only by the idle sweep
whose interval that same teardown disposes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A fresh, isolated CLAUDE_CONFIG_DIR (used to keep an injected API key
away from a stored claude.ai OAuth login) looks like a brand-new Claude
Code profile to the CLI, so it replays its ENTIRE first-run sequence on
every single launch: the theme picker, the security-notes screen, the
per-project "trust this folder?" dialog, and (running with
--dangerously-skip-permissions) a one-time bypass-permissions warning —
confirmed live, none of which a real, already-onboarded profile shows
again.
- New registry-declared env-kind field `skipFirstRunPrompts` (alongside
apiKeyTrustFile, which it reuses) — claude's entry only, carried
through buildCustomModelInjection (pure) into
applyCustomModelInjection (IO).
- seedFirstRunOnboardingState(): merges hasCompletedOnboarding: true and
this session's own projects[workingDir].hasTrustDialogAccepted: true
into the same <configDir>/.claude.json the API-key trust file already
writes to — other projects and other fields on this session's own
entry are left untouched.
- seedSkipBypassPermissionsPrompt(): merges
skipDangerousModePermissionPrompt: true into <configDir>/settings.json,
a separate file, same corrupt-tolerant merge behavior.
- applyCustomModelInjection() gains an optional workingDir parameter,
threaded from session.workingDir (dedicated apply route) /
resolvedCasePath (quick-start route) — boot recovery omits it
(a dialog already answered once needs no re-seed on the same,
persisted isolated directory).
Tests added at the pure-builder, IO-wrapper (including merge-preserves-
other-fields and corrupt-file-tolerance cases), and existing directory-
listing assertions updated for the new settings.json file. Typecheck/
lint/format clean; full suite shows no new regressions (baseline
pre-existing Windows-environment failures unchanged, 8 more passing
tests than before — the ones added here).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RqZeHrRS6DYcGcGX2p9EwG
The CLAUDE_CONFIG_DIR isolation from the previous commit fixed the cosmetic
auth warning but introduced a real regression: an otherwise-empty config
directory has none of a real profile's prior custom-API-key approvals, so
Claude Code stops at an interactive 'Detected a custom API key - use it?'
prompt on every single launch. Confirmed live. With nobody at a TTY to
answer, the prompt's own default ('No') silently refuses the very key this
feature just injected, which looks like the endpoint being ignored.
Adds apiKeyTrustFile to the env-kind customModelInjection capability shape
({relPath, shape: 'claude-api-key-responses'}), set on claude's entry to
{relPath: '.claude.json', shape: 'claude-api-key-responses'}. The apply step
merges customApiKeyResponses.approved: [apiKey] into
<isolatedConfigDir>/.claude.json - the exact field a real answered prompt
itself writes to (confirmed against a real ~/.claude.json after answering by
hand once), so this answers the prompt in advance rather than bypassing it.
Merges onto whatever the CLI already wrote into that file on an earlier
launch in the same isolated directory rather than overwriting it; a missing
or corrupt file is treated as empty rather than failing the apply.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RqZeHrRS6DYcGcGX2p9EwG
Addresses two live-validation findings on the Run-menu custom-model picker:
1. Both claude.ai and ANTHROPIC_API_KEY set warning. Claude Code still
coexists an OAuth login with an injected ANTHROPIC_API_KEY in the same
config directory and warns about it (confirmed cosmetic - the API key
wins for actual requests, verified via a real session's own API Usage
Billing line). A custom-model claude session now gets an isolated
CLAUDE_CONFIG_DIR (registry-declared via a new configDirVar field, empty,
no files written into it) so there is nothing to conflict with. projects
is symlinked (junction on Windows) back into the real config dir so the
response viewer, subagent windows and Read My Mind keep working for that
session, best-effort.
2. Context-window overflow. Claude Code assumes a large default context
window for a model id it doesn't recognise and never compacts, so a
custom endpoint's real, much smaller context (verified live: a 400
exceeding a 16384-token llama-swap model with a stock ~33.7K-token system
prompt) silently overflows. Discovery now also learns each model's real
context length from llama.cpp/llama-swap's GET /props?model=<id> (n_ctx),
but ONLY for a model llama-swap's own /v1/models response already marks
status.value === 'loaded' - never an unloaded one, since llama-swap
treats ?model= as a routing hint and probing an unloaded model risks
triggering an actual, slow, GPU-swapping load as a side effect of
read-only discovery. A server with no status field at all gets no
enrichment rather than a guess; a model not probed this round keeps its
previously-learned value until it disappears from the list entirely.
Stored per model (CustomModelHost.modelContextLengths) and applied via a
new contextLengthVar registry field, set to
CLAUDE_CODE_MAX_CONTEXT_TOKENS for claude.
Both new fields live on the existing env-kind customModelInjection
capability shape, declared only on claude's registry entry - every other
CLI's injection is unaffected (pinned by test).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RqZeHrRS6DYcGcGX2p9EwG