From a9a8a3fa2e63bea3d55a2c124a99b7c2bd24621d Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Mon, 14 Sep 2026 17:54:02 -0700 Subject: [PATCH] fix(config): `hermes config get` masks credentials on every path; `--raw` opts out `hermes config get providers`, `config get providers.
.api_key`, `config get
.env.X_API_KEY`
all printed the full credential. The agent runs this command from sessions whose transcripts
persist and get forwarded (a Gemini key surfaced in a Discord DM log), so `print` output is a
leak path the logging redactor never sees.
`get_config_value` now applies the structural masker used by `config show` before printing,
honouring `security.redact_secrets` (default on), with a `--raw` flag for operators/scripts
that need the real value. `_is_secret_config_key` extends the exact-name set with the same
`*_API_KEY / *_TOKEN / *_SECRET / *_PASSWORD` suffixes `_is_env_config_key` already routes to
.env, so env-map leaves under `mcp_servers.*.env` mask too, and the `config set` echo uses the
same predicate.
Slim redo of #84153 by @webtecnica (same direction: mask in get_config_value; dropped the
redact_url_query_params re-export and the separate redaction-enabled reader in favour of
agent.redact._redact_enabled, which already resolves the profile-scoped policy).
Fixes #110758
Fixes #84106
---
hermes_cli/config.py | 27 +++++++++++++----
hermes_cli/subcommands/config.py | 3 ++
tests/hermes_cli/test_set_config_value.py | 36 +++++++++++++++++++++++
website/docs/reference/cli-commands.md | 2 +-
4 files changed, 61 insertions(+), 7 deletions(-)
diff --git a/hermes_cli/config.py b/hermes_cli/config.py
index a543bb2810..fd0377b997 100644
--- a/hermes_cli/config.py
+++ b/hermes_cli/config.py
@@ -2753,6 +2753,15 @@ _SECRET_CONFIG_KEYS = frozenset({
"api_key", "apikey", "key", "token", "access_token", "refresh_token", "id_token",
"secret", "client_secret", "password", "passwd", "auth", "authorization",
"private_key", "bearer", "jwt"})
+# Env-map shapes (``mcp_servers..env.FOO_API_KEY``, ``GEMINI_API_KEY``) — the same suffixes
+# ``_is_env_config_key`` routes to .env. Suffix-only so ``token_count`` stays visible.
+_SECRET_CONFIG_KEY_SUFFIXES = ("_api_key", "_token", "_secret", "_password")
+
+
+def _is_secret_config_key(key: str) -> bool:
+ """Whether the LAST segment of a config key names a credential value."""
+ leaf = key.rsplit(".", 1)[-1].lower()
+ return leaf in _SECRET_CONFIG_KEYS or leaf.endswith(_SECRET_CONFIG_KEY_SUFFIXES)
def redact_config_value(value: Any, _depth: int = 0) -> Any:
@@ -2765,7 +2774,7 @@ def redact_config_value(value: Any, _depth: int = 0) -> Any:
if isinstance(value, dict):
return {
k: mask_secret(v)
- if isinstance(k, str) and k.lower() in _SECRET_CONFIG_KEYS and isinstance(v, str) and v
+ if isinstance(k, str) and _is_secret_config_key(k) and isinstance(v, str) and v
else redact_config_value(v, _depth + 1)
for k, v in value.items()}
if isinstance(value, list):
@@ -3521,7 +3530,7 @@ def set_config_value(key: str, value: str, force: bool = False):
# Mask the echoed value when the (possibly nested) key is credential-shaped, e.g.
# ``model.api_key`` (lowercase, so it misses the .env routing above).
_display_value = value
- if key.rsplit(".", 1)[-1].lower() in _SECRET_CONFIG_KEYS and isinstance(value, str) and value:
+ if _is_secret_config_key(key) and isinstance(value, str) and value:
from agent.redact import mask_secret
_display_value = mask_secret(value)
print(f"✓ Set {key} = {_display_value} in {config_path}")
@@ -3533,8 +3542,10 @@ def set_config_value(key: str, value: str, force: bool = False):
_print_unknown_key_notice(key, suggestion)
-def get_config_value(key: str, *, as_json: bool = False):
- """Print a resolved configuration value."""
+def get_config_value(key: str, *, as_json: bool = False, raw: bool = False):
+ """Print a resolved configuration value. Credentials are masked unless ``--raw`` or
+ ``security.redact_secrets: false``: ``print`` bypasses the log redactor, and the agent runs
+ this command from sessions whose transcripts persist (#84106, #110758)."""
if _is_env_config_key(key):
env_value = get_env_value(key.upper())
value = _MISSING if env_value is None else env_value
@@ -3547,6 +3558,10 @@ def get_config_value(key: str, *, as_json: bool = False):
if value is _MISSING:
_exit_invalid(f"Config key not set: {key}")
+ from agent.redact import _redact_enabled, mask_secret
+ if not raw and _redact_enabled():
+ value = mask_secret(value) if isinstance(value, str) and _is_secret_config_key(key) else redact_config_value(value)
+
print(_format_config_get_value(value, as_json=as_json))
@@ -3610,7 +3625,7 @@ def _run_write_command(fn, *args) -> None:
_exit_invalid(f"✗ {exc}")
-_USAGE_GET = ("Usage: hermes config get