{"record":{"id":"b935d1d45353302e","repo":"Hmbown/CodeWhale","slug":"refusing-to-save-an-empty-api-key","errorCode":null,"errorMessage":"Refusing to save an empty API key.","messagePattern":"Refusing to save an empty API key\\.","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/config.rs","lineNumber":11916,"sourceCode":"/// removed. When the secret-store write fails (OS permission denied, corrupt\n/// or read-only file backend, etc.), the save fails loudly rather than writing\n/// the key to plaintext `config.toml`.\n///\n/// Under `cfg(test)` the secret-store path is enabled only when the test sets\n/// both an isolated `CODEWHALE_HOME` and an explicit backend, preventing unit\n/// tests from touching the developer's real credential store.\npub fn save_api_key(api_key: &str) -> Result<SavedCredential> {\n    save_root_api_key_for_secret_slot(api_key, \"deepseek\", true)\n}\n\nfn save_root_api_key_for_secret_slot(\n    api_key: &str,\n    secret_slot: &str,\n    clear_deepseek_provider_slot: bool,\n) -> Result<SavedCredential> {\n    let trimmed = api_key.trim();\n    if trimmed.is_empty() {\n        anyhow::bail!(\"Refusing to save an empty API key.\");\n    }\n\n    let path = credential_config_path().context(\"Failed to resolve config path for API key.\")?;\n\n    if let Some(secrets) = credential_secret_store() {\n        // Same read-modify-write as the per-provider save below; hold the slot's\n        // write lock across snapshot, store write, config write, and rollback.\n        return crate::credentials::store::with_provider_write_lock(secret_slot, || {\n            let prior_secret = secrets.get(secret_slot);\n            match prior_secret.as_ref() {\n                Ok(prior) => match secrets.set(secret_slot, trimmed) {\n                    Ok(()) => {\n                        if let Err(error) = save_root_api_key_metadata_without_plaintext(\n                            &path,\n                            clear_deepseek_provider_slot,\n                        ) {\n                            let current = secrets.get(secret_slot).map_err(|rollback| {\n                        anyhow::anyhow!(","sourceCodeStart":11898,"sourceCodeEnd":11934,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/config.rs#L11898-L11934","documentation":"When persisting a provider credential, `save_api_key` first trims the supplied key and refuses to store an empty string. Saving an empty key would silently break authentication, so the operation fails fast with this error.","triggerScenarios":"Calling the API-key save routine (e.g. `codewhale auth save` / TUI provider key setup) with an empty or whitespace-only `api_key` string.","commonSituations":"Pressing Enter without pasting a key in the TUI; an env var like `OPENAI_API_KEY` unset so the passed value is empty; a script interpolating an unset variable.","solutions":["Provide a non-empty API key when prompted or via the environment","Check that the env var feeding the key is actually set (`echo $OPENAI_API_KEY`)","Re-run the save command with the key quoted to avoid shell stripping"],"exampleFix":"// before\nsave_api_key(\"\")\n// after\nlet key = std::env::var(\"OPENAI_API_KEY\")?;\nif key.trim().is_empty() { return Err(anyhow!(\"missing OPENAI_API_KEY\")); }\nsave_api_key(&key)","handlingStrategy":"validation","validationCode":"if api_key.trim().is_empty() {\n    return Err(\"provide a non-empty API key before saving\");\n}","typeGuard":"fn has_api_key(s: &str) -> bool { !s.trim().is_empty() }","tryCatchPattern":"match save_api_key(&key, slot, false) {\n    Err(e) if e.to_string().contains(\"empty API key\") => prompt_user_for_key(),\n    other => other?,\n}","preventionTips":["Check that the env var feeding the key is set before invoking save","Trim and verify user input in TUI prompt flows","Fail early with your own message when key input is blank"],"tags":["config","api-key","validation"],"backgroundTag":"empty-required-field","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}