{"repo":"NousResearch/hermes-agent","public_id":"17138","display_label":"env sanitizer corrupts keys sharing suffix substrings","summary":"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.","subsystem":null,"counts":{"members":6,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":4},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":17288},"resolution":{"state":"resolved","by":"pr-17288"},"freshness":{"state":"verified","last_verified_at":"2026-04-29T05:22:46+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.3333333333333333},"fix_edges":[{"issue_number":17138,"pr_number":17288,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true}],"members":[{"kind":"issue","number":17138,"title":"[Bug]: On start, Hermes Agent is santizing GLM_API_KEY/GLM_BASE_URL into G\\nLM_API_KEY and G\\nLM_BASE_URL","url":null,"state":"closed"},{"kind":"pr","number":17141,"title":"fix(cli): prevent .env sanitizer from splitting GLM_API_KEY by LM_API_KEY suffix","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#17288","number":17288}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":17241,"title":"fix(config): sanitize_env substring false positive breaks valid API keys","url":null,"state":"closed","close_reason":{"category":"implemented_elsewhere","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":17273,"title":"fix(config): prevent .env sanitizer from corrupting GLM_API_KEY and other suffix-trap keys","url":null,"state":"closed","close_reason":{"category":"implemented_elsewhere","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":17288,"title":"fix(cli): prevent .env sanitizer from splitting GLM_API_KEY by LM_API_KEY suffix","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["hermes_cli/config.py","tests/hermes_cli/test_config.py"],"fix_hunks":[{"file":"hermes_cli/config.py","hunks":["@@ -3710,18 +3710,27 @@ def _sanitize_env_lines(lines: list) -> list:\n \n         # Detect concatenated KEY=VALUE pairs on one line.\n         # Search for known KEY= patterns at any position in the line.\n-        split_positions = []\n+        # We collect full needle ranges so we can drop matches that are\n+        # fully contained within a longer overlapping needle. Without this,\n+        # suffix collisions corrupt the file: e.g. LM_API_KEY= inside\n+        # GLM_API_KEY= would otherwise split the line into \"G\\nLM_API_KEY=...\".\n+        match_ranges: list[tuple[int, int]] = []\n         for key_name in known_keys:\n             needle = key_name + \"=\"\n             idx = stripped.find(needle)\n             while idx >= 0:\n-                split_positions.append(idx)\n+                match_ranges.append((idx, idx + len(needle)))\n                 idx = stripped.find(needle, idx + len(needle))\n \n+        split_positions = sorted({\n+            s for s, e in match_ranges\n+            if not any(\n+                s2 <= s and e2 >= e and (s2, e2) != (s, e)\n+                for s2, e2 in match_ranges\n+            )\n+        })\n+\n         if len(split_positions) > 1:\n-            split_positions.sort()\n-            # Deduplicate (shouldn't happen, but be safe)\n-            split_positions = sorted(set(split_positions))\n             for i, pos in enumerate(split_positions):\n                 end = split_positions[i + 1] if i + 1 < len(split_positions) else len(stripped)\n                 part = stripped[pos:end].strip()"]},{"file":"tests/hermes_cli/test_config.py","hunks":["@@ -319,6 +319,23 @@ def test_value_ending_with_digits_still_splits(self):\n         assert result[0].startswith(\"OPENROUTER_API_KEY=\")\n         assert result[1].startswith(\"OPENAI_BASE_URL=\")\n \n+    def test_glm_suffix_collision_not_split(self):\n+        \"\"\"GLM_API_KEY / GLM_BASE_URL must not be mangled by LM_API_KEY / LM_BASE_URL suffixes (#17138).\"\"\"\n+        lines = [\n+            \"GLM_API_KEY=[redacted:secret-assignment]\",\n+            \"GLM_BASE_URL=https://api.z.ai/api/paas/v4\\n\",\n+        ]\n+        result = _sanitize_env_lines(lines)\n+        assert result == lines, f\"GLM_* lines were corrupted by suffix collision: {result}\"\n+\n+    def test_suffix_collision_does_not_break_real_concatenation(self):\n+        \"\"\"A genuine concatenation that happens to start with a suffix-superset key still splits.\"\"\"\n+        lines = [\"GLM_API_KEY=[redacted:secret-assignment]\"]\n+        result = _sanitize_env_lines(lines)\n+        assert len(result) == 2\n+        assert result[0].startswith(\"GLM_API_KEY=\")\n+        assert result[1].startswith(\"LM_API_KEY=\")\n+\n     def test_save_env_value_fixes_corruption_on_write(self, tmp_path):\n         \"\"\"save_env_value sanitizes corrupted lines when writing a new key.\"\"\"\n         env_file = tmp_path / \".env\"\n"]}]},{"kind":"pr","number":17291,"title":"fix(config): prevent _sanitize_env_lines from splitting valid keys via substring match","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"88e07c42b","number":0},{"kind":"pr","value":"#17288","number":17288},{"kind":"pr","value":"#17141","number":17141},{"kind":"pr","value":"#17138","number":17138},{"kind":"pr","value":"#17288","number":17288}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}