{"record":{"id":"9214f8529c2c3d6f","repo":"gitbutlerapp/gitbutler","slug":"pass-either-api-key-or-api-key-env-not-both","errorCode":null,"errorMessage":"Pass either --api-key or --api-key-env, not both","messagePattern":"Pass either --api-key or --api-key-env, not both","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/but/src/command/config.rs","lineNumber":1660,"sourceCode":"            t.sym().success,\n            t.config_value.paint(provider.display_name()),\n            t.hint.paint(scope.as_str())\n        )?;\n    } else if let Some(out) = out.for_json() {\n        out.write_value(serde_json::json!({\n            \"provider\": provider.as_git_config_value(),\n            \"scope\": scope.as_str(),\n        }))?;\n    }\n    Ok(())\n}\n\nfn resolve_secret_input(\n    api_key: Option<String>,\n    api_key_env: Option<String>,\n) -> Result<Option<Sensitive<String>>> {\n    if api_key.is_some() && api_key_env.is_some() {\n        anyhow::bail!(\"Pass either --api-key or --api-key-env, not both\")\n    }\n\n    if let Some(value) = api_key {\n        return Ok(Some(Sensitive(value)));\n    }\n\n    if let Some(env_name) = api_key_env {\n        let value = std::env::var(&env_name)\n            .with_context(|| format!(\"Environment variable '{env_name}' is not set\"))?;\n        return Ok(Some(Sensitive(value)));\n    }\n\n    Ok(None)\n}\n\nfn require_non_interactive_secret_if_byok(\n    key_option: AiKeyOption,\n    secret: Option<&Sensitive<String>>,","sourceCodeStart":1642,"sourceCodeEnd":1678,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/58e5313667b857ef39a730e380af31816a7b1768/crates/but/src/command/config.rs#L1642-L1678","documentation":"`resolve_secret_input` normalizes the two mutually exclusive ways of supplying an API secret non-interactively: `--api-key <value>` (inline) and `--api-key-env <var>` (read from environment). Passing both is treated as a caller mistake because the intended secret source is ambiguous, so it bails before reading either value.","triggerScenarios":"Invoking `but config ai ... --api-key X --api-key-env MY_VAR` (or the SDK equivalent passing both Some values to ai_config_non_interactive).","commonSituations":"Shell scripts accumulating flags from templates or defaults so both options end up set; users unsure which flag to use and passing both; config generators merging CLI profiles.","solutions":["Remove either --api-key or --api-key-env, keeping only one secret source","Prefer --api-key-env in scripts to avoid leaking the key into shell history and process listings","Audit wrapper scripts/aliases that may inject a default --api-key flag"],"exampleFix":"// before\nbut config ai --provider openai --api-key sk-1 --api-key-env OPENAI_API_KEY\n// after\nbut config ai --provider openai --api-key-env OPENAI_API_KEY","handlingStrategy":"validation","validationCode":"function assertSingleSecretSource({ apiKey, apiKeyEnv }) {\n  const n = [apiKey, apiKeyEnv].filter(Boolean).length;\n  if (n > 1) throw new Error('Pass either --api-key or --api-key-env, not both');\n}","typeGuard":"const hasExactlyOneSecret = (o) => ['apiKey','apiKeyEnv'].filter(k => o[k] != null).length === 1;","tryCatchPattern":"try {\n  await configureAi(args);\n} catch (e) {\n  if (e.message.includes('not both')) {\n    console.error('Strip one of --api-key / --api-key-env from your command template');\n  } else throw e;\n}","preventionTips":["Standardize on --api-key-env in scripts; never hardcode --api-key in wrappers","Check aliases/functions for injected default flags","Validate flag sets before composing CLI invocations"],"tags":["cli","configuration","flags"],"backgroundTag":"mutually-exclusive-flags","analyzedSha":"58e5313667b857ef39a730e380af31816a7b1768","analyzedAt":"2026-09-18T06:50:32.052Z","contentChangedAt":"2026-09-18T06:50:32.052Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}