Hmbown/CodeWhale · error · anyhow::Error
Custom provider '{route_name}' API key not found: the route
Error message
Custom provider '{route_name}' API key not found: the route binds api_key_env = "{env_name}" but that environment variable is not set. Set {env_name} to your key, or remove api_key_env from [providers.{route_name}] to run the endpoint without credentials. What it means
For ApiProvider::Custom routes that bind api_key_env, key resolution fails hard when the named environment variable is unset or empty. This closes #5104: previously a loopback endpoint silently dispatched unauthenticated while the operator believed credentials were wired. The error names the route and env var and offers the two valid escapes (set the var, or drop api_key_env).
Source
Thrown at crates/tui/src/config.rs:6236
// `[providers.<name>] api_key_env = "..."`. This remains safe for a
// custom endpoint because the binding belongs to that route; ambient
// provider variables below do not.
//
// For a custom provider, a binding that names an unset (or empty)
// variable is a broken credential contract, not a keyless route: fail
// loudly with the route-scoped fix instead of silently degrading to
// the self-hosted loopback keyless fallback below (#5104). Without
// this, an `api_key_env` route on a loopback host dispatched
// unauthenticated while the operator believed credentials were wired,
// and the composer-side preflight recovery never saw an error.
if provider == ApiProvider::Custom
&& let Some(env_name) = bound_provider_api_key_env_name(self, provider)
{
return match std::env::var(&env_name) {
Ok(value) if !value.trim().is_empty() => Ok(value),
_ => {
let route_name = self.provider.as_deref().unwrap_or("<name>");
Err(anyhow::anyhow!(
"Custom provider '{route_name}' API key not found: the route binds \
api_key_env = \"{env_name}\" but that environment variable is not set. \
Set {env_name} to your key, or remove api_key_env from \
[providers.{route_name}] to run the endpoint without credentials."
))
}
};
}
if let Some(value) = provider_config_env_api_key(self, provider) {
return Ok(value);
}
// 2. The dispatcher resolves this same provider slot before launching
// the TUI. Standalone `codewhale-tui` launches must see the identical
// durable credential. Auto-detection is file-backed and prompt-free by
// default; the OS keyring is queried only when the user explicitly
// selects the system backend.
if !self.should_skip_secret_store_for_provider(provider)View on GitHub (pinned to 8880682c63)
Solutions
- Export the variable in the environment that launches codewhale: export MYROUTE_API_KEY=... (prefer ~/.zshenv on zsh so non-interactive shells see it).
- Or put the key inline: [providers.myroute] api_key = "...".
- Or remove api_key_env from [providers.<route>] if the endpoint is genuinely keyless (e.g. loopback self-hosted).
Example fix
# before [providers.myroute] base_url = "https://api.example.com/v1" api_key_env = "MYROUTE_API_KEY" # $MYROUTE_API_KEY unset # after (option 1, shell) # export MYROUTE_API_KEY=sk-... # after (option 2, config) [providers.myroute] base_url = "https://api.example.com/v1" api_key = "sk-..."
Defensive patterns
Strategy: validation
Validate before calling
// before resolving the key, verify the bound env var is present and non-empty
if let Some(env_name) = bound_provider_api_key_env_name(&config, ApiProvider::Custom) {
let set = std::env::var(&env_name).map(|v| !v.trim().is_empty()).unwrap_or(false);
anyhow::ensure!(set, "route requires {env_name}; export it or drop api_key_env");
} Type guard
fn custom_route_key_env_is_set(cfg: &Config) -> bool {
bound_provider_api_key_env_name(cfg, ApiProvider::Custom)
.map(|n| std::env::var(&n).map(|v| !v.trim().is_empty()).unwrap_or(false))
.unwrap_or(true) // no binding → nothing to guard
} Try / catch
match resolve_key(&config) {
Err(e) if e.to_string().contains("api_key_env") && e.to_string().contains("is not set") => {
// prompt to export the var, or fall back to keyless loopback by removing api_key_env
Err(e)
}
other => other,
} Prevention
- On zsh, export route secrets in ~/.zshenv (non-interactive shells read it; ~/.zshrc does not).
- Wire the env var into every service unit/CI job that launches codewhale.
- Prefer inline api_key for machines where env plumbing is fragile.
When it happens
Trigger: [providers.myroute] api_key_env = "MYROUTE_API_KEY" with the variable not exported in the launching shell; CI/daemon/service units that lack the env var; empty-string value (also rejected, since trim-empty fails).
Common situations: Export defined in ~/.zshrc (interactive shells only) instead of ~/.zshenv; secrets configured in one CI job but not the deployed one; renaming the env var in config without updating the environment.
Related errors
- Custom endpoint credentials for {route_name} must be bound e
- unavailable credential
- Secret storage write failed for {slot}: {err}. Refusing to w
- Invalid default_text_model '{model}' for provider '{}': expe
- Invalid approval_policy '{policy}': expected on-request, unt
AI-assisted analysis of Hmbown/CodeWhale@8880682c63 (2026-08-16).
Data as JSON: /api/errors/180ab91c84a75935.
Report an issue: GitHub.