fix(cli): store custom endpoint API key in .env instead of config.yaml - #57557
fix(cli): store custom endpoint API key in .env instead of config.yaml#57557liuhao1024 wants to merge 1 commit into
Conversation
When configuring a Custom endpoint via `hermes model`, the API key was
stored directly in config.yaml. This leaks credentials into the model
context when Hermes edits its own config.
Derive an env var name from the hostname (e.g. api.featherless.ai ->
FEATHERLESS_API_KEY), save the key to ~/.hermes/.env, and store a
${VAR} reference in config.yaml. The existing _expand_env_vars()
mechanism resolves these at runtime.
Fixes NousResearch#57547
|
suggesting changes Security evidence: the core fix moves custom endpoint API keys from Please namespace derived env vars for custom endpoints so they cannot equal provider-global credentials, while keeping distinct custom hosts stable and unique. Add regression coverage for a host that currently collides with an existing provider env var, and fix the trailing blank line before the next pass. Signed: GPT-5.5-xhigh in Codex |
|
Thanks for the thorough security review, egilewski. You're right — Plan:
Will push a fix shortly. |
teknium1
left a comment
There was a problem hiding this comment.
Thanks for moving a real plaintext-secret persistence path out of config.yaml; current main still writes the key at hermes_cli/model_setup_flows.py:955 and hermes_cli/main.py:3827.
Problems
hermes_cli/model_setup_flows.py:66derivesOPENAI_API_KEYforhttps://api.openai.com/v1, and line 815 writes the custom key to that name. This can replace a standard provider credential. The collision is also acknowledged in the PR discussion.- Prefix/TLD stripping also makes different custom endpoints share a name; a namespace prefix alone does not make the derivation unique.
Suggested changes
- Use a custom-only, deterministic name based on the full endpoint identity (or a stable digest), so it cannot equal provider-global keys or another custom endpoint's key.
- Add regressions for preserving
OPENAI_API_KEYand for distinct URLs that normalize to the same current name. Add a temp-HERMES_HOMEpersistence/resolution test rather than only mockingsave_env_value.
Automated hermes-sweeper review.
| if not hostname: | ||
| hostname = "custom" | ||
|
|
||
| return f"{hostname.upper()}_API_KEY" |
There was a problem hiding this comment.
This derives OPENAI_API_KEY for https://api.openai.com/v1 (and similarly provider-global names for other hosts). Line 815 then overwrites that variable in .env with the custom-endpoint credential. Please use a custom-only, endpoint-unique name; a HERMES_CUSTOM_ prefix alone must also retain enough of the full endpoint identity to avoid custom-to-custom collisions.
|
Hi liuhao1024! Thanks for this fix — I ran into the same issue from the Desktop UI side and traced the code path before discovering your PR here. Your fix covers the CLI path (model_setup_flows.py + main.py), but the Desktop settings panel uses a separate code path in web_server.py (_write_custom_endpoint) that still stores the key in plaintext. I submitted PR #69488 to cover that path, using the same key_env + save_env_value approach. Not competing — just complementing. Both PRs together should fully resolve #57547 and #69449. |
|
Thanks @liuhao1024 — you were first on this, back in early July, and you caught the write path the other PRs missed: One thing worth flagging, since your own test asserted it: Closing as superseded. |
What does this PR do?
When configuring a "Custom endpoint" via
hermes model, the API key was storeddirectly in
config.yamlundermodel.api_keyandcustom_providers[].api_key.This is a security concern: when Hermes edits its own config file, the raw API key
can leak into the model's context window.
This fix derives an environment variable name from the endpoint hostname (e.g.
api.featherless.ai→FEATHERLESS_API_KEY), saves the key to~/.hermes/.env,and stores a
${VAR}reference inconfig.yamlinstead. The existing_expand_env_vars()mechanism inload_config()resolves these references atruntime.
Related Issue
Fixes #57547
Type of Change
Changes Made
hermes_cli/model_setup_flows.py: Added_derive_custom_endpoint_env_var()helper that extracts the hostname from a custom endpoint URL, strips common prefixes/TLDs, and produces anUPPER_SNAKE_CASE_API_KEYenv var name.hermes_cli/model_setup_flows.py: Modified_model_flow_custom()to save the API key to~/.hermes/.envviasave_env_value()and store${ENV_VAR}reference inconfig.yamlinstead of the raw key.hermes_cli/main.py: Extended_save_custom_provider()with akey_envparameter. When provided, storeskey_env(not rawapi_key) in thecustom_providersentry. The runtime already resolveskey_envviaos.getenv()inauxiliary_client.py.tests/cli/test_cli_provider_resolution.py: Updatedtest_model_flow_custom_persists_selected_api_modeto expect${CODEX_EXAMPLE_API_KEY}instead of the raw key, and to verifykey_envpropagation. Addedtest_derive_custom_endpoint_env_varcovering 8 URL patterns.How to Test
python -m pytest tests/cli/test_cli_provider_resolution.py -x -q— all 24 tests should pass.python -m pytest tests/hermes_cli/test_runtime_provider_resolution.py -x -q -k "custom"— all 48 tests should pass.hermes model, select "Custom endpoint", enter a URL and API key. Verify the key appears in~/.hermes/.env(not inconfig.yaml) and that${ENV_VAR}is stored inconfig.yamlundermodel.api_key.Checklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/cli/test_cli_provider_resolution.py -qand all 24 tests passDocumentation & Housekeeping
docs/, docstrings) — or N/Acli-config.yaml.exampleif I added/changed config keys — or N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — or N/Aurllib.parseandsave_env_valueare cross-platform)