# NousResearch/hermes-agent · env sanitizer corrupts keys sharing suffix substrings

## Open now

- Nothing open on GitHub.

A bug where the .env sanitizer incorrectly splits keys like GLM_API_KEY because they contain a known suffix (LM_API_KEY) as a substring, along with multiple proposed fixes for the substring matching logic.

## TL;DR

- 6 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 4 PR(s) closed without merging.
- A fix was merged to main in PR #17288. Verify it addresses your case.
- **Recommended:** read PR #17288 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `resolved` · deciding PR(s): `pr-17288`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.33`: the share of member 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.

## Closed / duplicate issues

- #17138: \[Bug\]: On start, Hermes Agent is santizing GLM_API_KEY/GLM_BASE_URL into G\\nLM_API_KEY and G\\nLM_BASE_URL

## Prior & competing PRs

- **#17241: fix(config): sanitize_env substring false positive breaks valid API keys**. closed without merge · fix tier `suggested`
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#17273: fix(config): prevent .env sanitizer from corrupting GLM_API_KEY and other suffix-trap keys**. closed without merge · fix tier `suggested`
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#17288: fix(cli): prevent .env sanitizer from splitting GLM_API_KEY by LM_API_KEY suffix**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `hermes_cli/config.py`, `tests/hermes_cli/test_config.py`
  - fix hunks:
  - `hermes_cli/config.py`

    ```diff
    @@ -3710,18 +3710,27 @@ def _sanitize_env_lines(lines: list) -> list:
     
             # Detect concatenated KEY=VALUE pairs on one line.
             # Search for known KEY= patterns at any position in the line.
    -        split_positions = []
    +        # We collect full needle ranges so we can drop matches that are
    +        # fully contained within a longer overlapping needle. Without this,
    +        # suffix collisions corrupt the file: e.g. LM_API_KEY= inside
    +        # GLM_API_KEY= would otherwise split the line into "G\nLM_API_KEY=...".
    +        match_ranges: list[tuple[int, int]] = []
             for key_name in known_keys:
                 needle = key_name + "="
                 idx = stripped.find(needle)
                 while idx >= 0:
    -                split_positions.append(idx)
    +                match_ranges.append((idx, idx + len(needle)))
                     idx = stripped.find(needle, idx + len(needle))
     
    +        split_positions = sorted({
    +            s for s, e in match_ranges
    +            if not any(
    +                s2 <= s and e2 >= e and (s2, e2) != (s, e)
    +                for s2, e2 in match_ranges
    +            )
    +        })
    +
             if len(split_positions) > 1:
    -            split_positions.sort()
    -            # Deduplicate (shouldn't happen, but be safe)
    -            split_positions = sorted(set(split_positions))
                 for i, pos in enumerate(split_positions):
                     end = split_positions[i + 1] if i + 1 < len(split_positions) else len(stripped)
                     part = stripped[pos:end].strip()
    ```
  - `tests/hermes_cli/test_config.py`

    ```diff
    @@ -319,6 +319,23 @@ def test_value_ending_with_digits_still_splits(self):
             assert result[0].startswith("OPENROUTER_API_KEY=")
             assert result[1].startswith("OPENAI_BASE_URL=")
     
    +    def test_glm_suffix_collision_not_split(self):
    +        """GLM_API_KEY / GLM_BASE_URL must not be mangled by LM_API_KEY / LM_BASE_URL suffixes (#17138)."""
    +        lines = [
    +            "GLM_API_KEY=[redacted:secret-assignment]",
    +            "GLM_BASE_URL=https://api.z.ai/api/paas/v4\n",
    +        ]
    +        result = _sanitize_env_lines(lines)
    +        assert result == lines, f"GLM_* lines were corrupted by suffix collision: {result}"
    +
    +    def test_suffix_collision_does_not_break_real_concatenation(self):
    +        """A genuine concatenation that happens to start with a suffix-superset key still splits."""
    +        lines = ["GLM_API_KEY=[redacted:secret-assignment]"]
    +        result = _sanitize_env_lines(lines)
    +        assert len(result) == 2
    +        assert result[0].startswith("GLM_API_KEY=")
    +        assert result[1].startswith("LM_API_KEY=")
    +
         def test_save_env_value_fixes_corruption_on_write(self, tmp_path):
             """save_env_value sanitizes corrupted lines when writing a new key."""
             env_file = tmp_path / ".env"
    ```
- **#17141: fix(cli): prevent .env sanitizer from splitting GLM_API_KEY by LM_API_KEY suffix**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#17288`
- **#17291: fix(config): prevent _sanitize_env_lines from splitting valid keys via substring match**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `88e07c42b`, `#17288`, `#17141`, `#17138`, `#17288`

## Fix status

- Merged to main in PR #17288 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-04-29`.
