{"repo":"NousResearch/hermes-agent","public_id":"8908","display_label":"Telegram bot token duplication bug and fix","summary":"A bug causes the Telegram bot token to be duplicated multiple times, resulting in an InvalidToken error. A fix involving sanitizing the .env file before loading is proposed to address this.","subsystem":null,"counts":{"members":3,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":8974,"merged_at":"2026-04-13T11:35:37+00:00"},"resolution":{"state":"resolved","by":"pr-8974"},"freshness":{"state":"stale","last_verified_at":"2026-04-13T11:35:37+00:00","is_stale":true},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":8908,"pr_number":8974,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-04-13T11:35:37+00:00"}],"members":[{"kind":"issue","number":8908,"title":"Bug: Telegram bot token gets duplicated 8 times causing InvalidToken error","url":null,"state":"closed"},{"kind":"pr","number":8939,"title":"fix: sanitize .env before loading to prevent token duplication (#8908)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#8974","number":8974}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":8974,"title":"fix: sanitize .env before loading to prevent token duplication (#8908)","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["hermes_cli/config.py","hermes_cli/env_loader.py","tests/hermes_cli/test_env_sanitize_on_load.py"],"fix_hunks":[{"file":"hermes_cli/config.py","hunks":["@@ -2384,7 +2384,13 @@ def save_config(config: Dict[str, Any]):\n \n \n def load_env() -> Dict[str, str]:\n-    \"\"\"Load environment variables from ~/.hermes/.env.\"\"\"\n+    \"\"\"Load environment variables from ~/.hermes/.env.\n+\n+    Sanitizes lines before parsing so that corrupted files (e.g.\n+    concatenated KEY=VALUE pairs on a single line) are handled\n+    gracefully instead of producing mangled values such as duplicated\n+    bot tokens.  See #8908.\n+    \"\"\"\n     env_path = get_env_path()\n     env_vars = {}\n     ","@@ -2393,17 +2399,21 @@ def load_env() -> Dict[str, str]:\n         # fail on UTF-8 .env files. Use explicit UTF-8 only on Windows.\n         open_kw = {\"encoding\": \"utf-8\", \"errors\": \"replace\"} if _IS_WINDOWS else {}\n         with open(env_path, **open_kw) as f:\n-            for line in f:\n-                line = line.strip()\n-                if line and not line.startswith('#') and '=' in line:\n-                    key, _, value = line.partition('=')\n-                    env_vars[key.strip()] = value.strip().strip('\"\\'')\n+            raw_lines = f.readlines()\n+        # Sanitize before parsing: split concatenated lines & drop stale\n+        # placeholders so corrupted .env files don't produce invalid tokens.\n+        lines = _sanitize_env_lines(raw_lines)\n+        for line in lines:\n+            line = line.strip()\n+            if line and not line.startswith('#') and '=' in line:\n+                key, _, value = line.partition('=')\n+                env_vars[key.strip()] = value.strip().strip('\"\\'')\n     \n     return env_vars\n \n \n def _sanitize_env_lines(lines: list) -> list:\n-    \"\"\"Fix corrupted .env lines before writing.\n+    \"\"\"Fix corrupted .env lines before reading or writing.\n \n     Handles two known corruption patterns:\n     1. Concatenated KEY=VALUE pairs on a single line (missing newline between"]},{"file":"hermes_cli/env_loader.py","hunks":["@@ -15,6 +15,51 @@ def _load_dotenv_with_fallback(path: Path, *, override: bool) -> None:\n         load_dotenv(dotenv_path=path, override=override, encoding=\"latin-1\")\n \n \n+def _sanitize_env_file_if_needed(path: Path) -> None:\n+    \"\"\"Pre-sanitize a .env file before python-dotenv reads it.\n+\n+    python-dotenv does not handle corrupted lines where multiple\n+    KEY=VALUE pairs are concatenated on a single line (missing newline).\n+    This produces mangled values — e.g. a bot token duplicated 8×\n+    (see #8908).\n+\n+    We delegate to ``hermes_cli.config._sanitize_env_lines`` which\n+    already knows all valid Hermes env-var names and can split\n+    concatenated lines correctly.\n+    \"\"\"\n+    if not path.exists():\n+        return\n+    try:\n+        from hermes_cli.config import _sanitize_env_lines\n+    except ImportError:\n+        return  # early bootstrap — config module not available yet\n+\n+    read_kw = {\"encoding\": \"utf-8\", \"errors\": \"replace\"}\n+    try:\n+        with open(path, **read_kw) as f:\n+            original = f.readlines()\n+        sanitized = _sanitize_env_lines(original)\n+        if sanitized != original:\n+            import tempfile\n+            fd, tmp = tempfile.mkstemp(\n+                dir=str(path.parent), suffix=\".tmp\", prefix=\".env_\"\n+            )\n+            try:\n+                with os.fdopen(fd, \"w\", encoding=\"utf-8\") as f:\n+                    f.writelines(sanitized)\n+                    f.flush()\n+                    os.fsync(f.fileno())\n+                os.replace(tmp, path)\n+            except BaseException:\n+                try:\n+                    os.unlink(tmp)\n+                except OSError:\n+                    pass\n+                raise\n+    except Exception:\n+        pass  # best-effort — don't block gateway startup\n+\n+\n def load_hermes_dotenv(\n     *,\n     hermes_home: str | os.PathLike | None = None,","@@ -34,6 +79,10 @@ def load_hermes_dotenv(\n     user_env = home_path / \".env\"\n     project_env_path = Path(project_env) if project_env else None\n \n+    # Fix corrupted .env files before python-dotenv parses them (#8908).\n+    if user_env.exists():\n+        _sanitize_env_file_if_needed(user_env)\n+\n     if user_env.exists():\n         _load_dotenv_with_fallback(user_env, override=True)\n         loaded.append(user_env)"]},{"file":"tests/hermes_cli/test_env_sanitize_on_load.py","hunks":["@@ -0,0 +1,91 @@\n+\"\"\"Tests for .env sanitization during load to prevent token duplication (#8908).\"\"\"\n+\n+import tempfile\n+from pathlib import Path\n+from unittest.mock import patch\n+\n+\n+def test_load_env_sanitizes_concatenated_lines():\n+    \"\"\"Verify load_env() splits concatenated KEY=VALUE pairs.\n+\n+    Reproduces the scenario from #8908 where a corrupted .env file\n+    contained multiple tokens on a single line, causing the bot token\n+    to be duplicated 8 times.\n+    \"\"\"\n+    from hermes_cli.config import load_env\n+\n+    token = \"[redacted:secret-assignment]\"\n+    # Simulate concatenated line: TOKEN=xxx followed immediately by another key\n+    corrupted = f\"TELEGRAM_BOT_TOKEN=[redacted:secret-assignment]\"\n+\n+    with tempfile.NamedTemporaryFile(\n+        mode=\"w\", suffix=\".env\", delete=False, encoding=\"utf-8\"\n+    ) as f:\n+        f.write(corrupted)\n+        env_path = Path(f.name)\n+\n+    try:\n+        with patch(\"hermes_cli.config.get_env_path\", return_value=env_path):\n+            result = load_env()\n+        assert result.get(\"TELEGRAM_BOT_TOKEN\") == token, (\n+            f\"Token should be exactly '{token}', got '{result.get('TELEGRAM_BOT_TOKEN')}'\"\n+        )\n+        assert result.get(\"ANTHROPIC_API_KEY\") == \"sk-ant-test123\"\n+    finally:\n+        env_path.unlink(missing_ok=True)\n+\n+\n+def test_load_env_normal_file_unchanged():\n+    \"\"\"A well-formed .env file should be parsed identically.\"\"\"\n+    from hermes_cli.config import load_env\n+\n+    content = (\n+        \"TELEGRAM_BOT_TOKEN=[redacted:secret-assignment]\"\n+        \"ANTHROPIC_API_KEY=[redacted:secret-assignment]\"\n+        \"# comment\\n\"\n+        \"\\n\"\n+        \"OPENAI_API_KEY=[redacted:secret-assignment]\"\n+    )\n+\n+    with tempfile.NamedTemporaryFile(\n+        mode=\"w\", suffix=\".env\", delete=False, encoding=\"utf-8\"\n+    ) as f:\n+        f.write(content)\n+        env_path = Path(f.name)\n+\n+    try:\n+        with patch(\"hermes_cli.config.get_env_path\", return_value=env_path):\n+            result = load_env()\n+        assert result[\"TELEGRAM_BOT_TOKEN\"] == \"mytoken123\"\n+        assert result[\"ANTHROPIC_API_KEY\"] == \"sk-ant-key\"\n+        assert result[\"OPENAI_API_KEY\"] == \"sk-openai\"\n+    finally:\n+        env_path.unlink(missing_ok=True)\n+\n+\n+def test_env_loader_sanitizes_before_dotenv():\n+    \"\"\"Verify env_loader._sanitize_env_file_if_needed fixes corrupted files.\"\"\"\n+    from hermes_cli.env_loader import _sanitize_env_file_if_needed\n+\n+    token = \"[redacted:secret-assignment]\"\n+    corrupted = f\"TELEGRAM_BOT_TOKEN=[redacted:secret-assignment]\"\n+\n+    with tempfile.NamedTemporaryFile(\n+        mode=\"w\", suffix=\".env\", delete=False, encoding=\"utf-8\"\n+    ) as f:\n+        f.write(corrupted)\n+        env_path = Path(f.name)\n+\n+    try:\n+        _sanitize_env_file_if_needed(env_path)\n+        with open(env_path, encoding=\"utf-8\") as f:\n+            lines = f.readlines()\n+        # Should be split into two separate lines\n+        assert len(lines) == 2, f\"Expected 2 lines, got {len(lines)}: {lines}\"\n+        assert lines[0].startswith(\"TELEGRAM_BOT_TOKEN=\")\n+        assert lines[1].startswith(\"ANTHROPIC_API_KEY=\")\n+        # Token should not contain the second key\n+        parsed_token = [redacted:secret-assignment]\"=\", 1)[1]\n+        assert parsed_token == token\n+    finally:\n+        env_path.unlink(missing_ok=True)\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}