NousResearch/hermes-agent

LLM token redaction incorrectly masking env variable lookups

Cluster 2852 · agent. Data as of

Nothing open on GitHub. Fix merged in #58915 on 5 Jul 2026. Check it covers your case.

The short answer

0open issues
0open PRs
4items grouped
Known?
Yes. 4 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #58915. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this resolved, deciding PR #58534. That is separate from the fix event.
Still open
Nothing open on GitHub.

A bug where the redaction logic for LLM tokens or secrets incorrectly masks programmatic environment variable lookups (e.g., os.getenv, os.environ) and related KEY=value patterns. Multiple fixes attempt to exempt env-lookup expressions from redaction rules that were intended for assignment or config contexts.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #58534 fix(redact): don't mask programmatic env lookups in KEY=value redaction (salvage #2854)

    merged to main

    files: agent/redact.py, tests/agent/test_redact.py

    Show the diff
    • agent/redact.py
      @@ -137,6 +137,14 @@
       # The colon-form URL guard (skip when ``://`` present) lives at the call site.
       _SECRET_CFG_NAMES = r"(?:api[ _.\-]?key|token|secret|passwd|password|credential|auth)"
       _CFG_VALUE = r"(['\"]?)([^\s&]+?)\2(?=[\s&]|$)"
      +
      +# Programmatic env lookups (``os.getenv(...)``, ``os.environ[...]``,
      +# ``os.environ.get(...)``, ``process.env.X``, ``$ENV{X}``) reference variable
      +# *names*, not secret values. When one appears as the VALUE of a KEY=... match
      +# it's a code snippet, not a leaked secret — skip redaction (issue #2852).
      +_ENV_LOOKUP_VALUE_RE = re.compile(
      +    r"^(?:os\.(?:getenv|environ)|process\.env|\$ENV\{)"
      +)
       # Namespaced (dotted) key: the secret word may sit anywhere in a dotted path.
       _CFG_DOTTED_RE = re.compile(
           rf"((?:[A-Za-z0-9_\-]+\.)+[A-Za-z0-9_.\-]*{_SECRET_CFG_NAMES}[A-Za-z0-9_.\-]*"
      @@ -541,6 +549,11 @@ def redact_sensitive_text(
               if "=" in text:
                   def _redact_env(m):
                       name, quote, value = m.group(1), m.group(2), m.group(3)
      +                # Programmatic env lookups reference variable *names*, not
      +                # secret values — masking them corrupts code snippets in
      +                # prose/log contexts (issue #2852): ``KEY=os.getenv('X')``.
      +                if _ENV_LOOKUP_VALUE_RE.match(value):
      +                    return m.group(0)
                       return f"{name}={quote}{_mask_token(value)}{quote}"
                   text = _ENV_ASSIGN_RE.sub(_redact_env, text)
                   # Lowercase/dotted config keys (issue #16413). Skip URLs entirely —
    • tests/agent/test_redact.py
      @@ -124,6 +124,52 @@ def test_export_whitespace_preserved(self):
               assert "mypassword" not in result
       
       
      +class TestEnvLookupPreserved:
      +    """Programmatic env var lookups must not be corrupted (issue #2852)."""
      +
      +    def test_os_getenv_single_quote_uppercase_key(self):
      +        text = "MY_API_KEY=os.getenv('OPENAI_API_KEY')"
      +        assert redact_sensitive_text(text, force=True) == text
      +
      +    def test_os_getenv_lowercase_config_key(self):
      +        text = "ha_token=os.getenv('HOMEASSISTANT_TOKEN')"
      +        assert redact_sensitive_text(text, force=True) == text
      +
      +    def test_os_getenv_double_quote(self):
      +        text = 'API_TOKEN=os.getenv("MY_API_TOKEN")'
      +        assert redact_sensitive_text(text, force=True) == text
      +
      +    def test_os_environ_get(self):
      +        text = "HA_TOKEN=os.environ.get('HOMEASSISTANT_TOKEN')"
      +        assert redact_sensitive_text(text, force=True) == text
      +
      +    def test_os_environ_bracket(self):
      +        text = "MY_SECRET=[redacted:secret-assignment]'MY_SECRET']"
      +        assert redact_sensitive_text(text, force=True) == text
      +
      +    def test_process_env(self):
      +        text = "api_key=[redacted:secret-assignment]"
      +        assert redact_sensitive_text(text, force=True) == text
      +
      +    def test_real_env_value_still_redacted(self):
      +        text = "HOMEASSISTANT_TOKEN=[redacted:secret-assignment]"
      +        result = redact_sensitive_text(text, force=True)
      +        assert "eyJhbGciOiJIUzI1NiJ9" not in result
      +
      +    def test_real_lowercase_value_still_redacted(self):
      +        text = "password=[redacted:secret-assignment]"
      +        result = redact_sensitive_text(text, force=True)
      +        assert "hunter2hunter2" not in result
      +
      +    def test_multiline_prose_with_code_snippet(self):
      +        text = """Set it up like this:
      +    HA_TOKEN=os.getenv('HOMEASSISTANT_TOKEN')
      +    if not HA_TOKEN:
      +        raise ValueError('Missing credentials')"""
      +        result = redact_sensitive_text(text, force=True)
      +        assert "os.getenv('HOMEASSISTANT_TOKEN')" in result
      +
      +
       class TestJsonFields:
           def test_json_api_key(self):
               text = '{"apiKey": "[redacted:openai-key]"}'
  • #58915 fix(redact): skip env-lookup exception for JSON/YAML config field redaction

    merged to main

    files: agent/redact.py, tests/agent/test_redact.py

    Show the diff
    • agent/redact.py
      @@ -570,6 +570,11 @@ def _redact_env(m):
               if ":" in text and '"' in text:
                   def _redact_json(m):
                       key, value = m.group(1), m.group(2)
      +                # Same programmatic-env-lookup exception as _redact_env above
      +                # (issue #2852): "apiKey": "os.getenv('X')" is a code snippet,
      +                # not a leaked secret value.
      +                if _ENV_LOOKUP_VALUE_RE.match(value):
      +                    return m.group(0)
                       return f'{key}: "{_mask_token(value)}"'
                   text = _JSON_FIELD_RE.sub(_redact_json, text)
       
      @@ -579,6 +584,11 @@ def _redact_json(m):
               if ":" in text and "://" not in text:
                   def _redact_yaml(m):
                       key, sep, value = m.group(1), m.group(2), m.group(3)
      +                # Same programmatic-env-lookup exception as _redact_env above
      +                # (issue #2852): api_key: os.getenv('X') is a code snippet,
      +                # not a leaked secret value.
      +                if _ENV_LOOKUP_VALUE_RE.match(value):
      +                    return m.group(0)
                       return f"{key}{sep}{_mask_token(value)}"
                   text = _YAML_ASSIGN_RE.sub(_redact_yaml, text)
       
    • tests/agent/test_redact.py
      @@ -185,6 +185,34 @@ def test_multiline_prose_with_code_snippet(self):
               result = redact_sensitive_text(text, force=True)
               assert "os.getenv('HOMEASSISTANT_TOKEN')" in result
       
      +    def test_json_field_os_getenv_preserved(self):
      +        # _redact_env has the env-lookup exception; _redact_json (a separate
      +        # closure, JSON key: "value" syntax) did not, and mangled this into
      +        # '"apiKey": "[redacted:secret-assignment]')"'.
      +        text = '{"apiKey": "[redacted:secret-assignment]'OPENAI_API_KEY\')"}'
      +        assert redact_sensitive_text(text, force=True) == text
      +
      +    def test_json_field_os_environ_get_preserved(self):
      +        text = '{"token": "[redacted:secret-assignment]'MY_TOKEN\')"}'
      +        assert redact_sensitive_text(text, force=True) == text
      +
      +    def test_json_field_real_value_still_redacted(self):
      +        text = '{"apiKey": "[redacted:openai-key]"}'
      +        result = redact_sensitive_text(text, force=True)
      +        assert "[redacted:openai-key]" not in result
      +
      +    def test_yaml_field_os_getenv_preserved(self):
      +        # Same exception missing from _redact_yaml (unquoted key: value
      +        # syntax) — mangled 'api_key: os.getenv("OPENAI_API_KEY")' into
      +        # 'api_key: [redacted:secret-assignment]")'.
      +        text = 'api_key: os.getenv("OPENAI_API_KEY")'
      +        assert redact_sensitive_text(text, force=True) == text
      +
      +    def test_yaml_field_real_value_still_redacted(self):
      +        text = "api_key: [redacted:openai-key]"
      +        result = redact_sensitive_text(text, force=True)
      +        assert "[redacted:openai-key]" not in result
      +
       
       class TestJsonFields:
           def test_json_api_key(self):

1 PR closed without merging

From the close comments: other 1

  • #2854 fix(redact): skip env-assignment redaction for os.getenv/os.environ lookups (#2852) · other → #2852
Closed issues · 1
  • Closed#2852[Bug]: LLM token redaction breaking on .env handling on scripts
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.50: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
agent
Merged PRs
#58534, #58915
Closed issues
1
Attempts
1 PR closed without merging
Fix link checked
5 Jul 2026

For your agent

The same facts, typed.