Quote .env values to prevent hash truncation

NousResearch/hermes-agent · cluster 30355 · hermes cli

Data as of

Nothing open on GitHub. Fix merged in #52653. Check it covers your case.

The short answer

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

These issues and PRs address a bug where special characters like # in .env values (e.g., API tokens) cause truncation when written to .env files. The fix involves quoting .env values to preserve their full content.

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.

  • #52653 fix(config): quote .env values containing # to prevent token truncation (#30355)

    merged to main

    files: hermes_cli/config.py, tests/hermes_cli/test_config.py

    Show the diff
    • hermes_cli/config.py
      @@ -6095,6 +6095,29 @@ def save_config(config: Dict[str, Any]):
               _LAST_EXPANDED_CONFIG_BY_PATH[str(config_path)] = copy.deepcopy(current_normalized)
       
       
      +def _parse_env_value(raw_value: str) -> str:
      +    """Parse the small .env value subset Hermes writes itself."""
      +    value = raw_value.strip()
      +    if len(value) >= 2 and value[0] == value[-1] == '"':
      +        quoted = value[1:-1]
      +        parsed: list[str] = []
      +        i = 0
      +        while i < len(quoted):
      +            ch = quoted[i]
      +            if ch == "\\" and i + 1 < len(quoted):
      +                next_ch = quoted[i + 1]
      +                if next_ch in {'"', "\\"}:
      +                    parsed.append(next_ch)
      +                    i += 2
      +                    continue
      +            parsed.append(ch)
      +            i += 1
      +        return "".join(parsed)
      +    if len(value) >= 2 and value[0] == value[-1] == "'":
      +        return value[1:-1]
      +    return value
      +
      +
       def load_env() -> Dict[str, str]:
           """Load environment variables from ~/.hermes/.env.
       
      @@ -6144,7 +6167,7 @@ def load_env() -> Dict[str, str]:
                   line = line.strip()
                   if line and not line.startswith('#') and '=' in line:
                       key, _, value = line.partition('=')
      -                env_vars[key.strip()] = value.strip().strip('"\'')
      +                env_vars[key.strip()] = _parse_env_value(value)
       
           if cache_key is not None:
               _env_cache = (cache_key, dict(env_vars))
      @@ -6318,6 +6341,22 @@ def _check_non_ascii_credential(key: str, value: str) -> str:
           return sanitized
       
       
      +def _quote_env_value(value: str) -> str:
      +    """Quote .env values containing characters with special dotenv meaning."""
      +    if value == "":
      +        return value
      +    needs_quoting = (
      +        "#" in value
      +        or '"' in value
      +        or "'" in value
      +        or value != value.strip()
      +    )
      +    if not needs_quoting:
      +        return value
      +    escaped = value.replace("\\", "\\\\").replace('"', '\\"')
      +    return f'"{escaped}"'
      +
      +
       def save_env_value(key: str, value: str):
           """Save or update a value in ~/.hermes/.env."""
           if is_managed():
      @@ -6357,11 +6396,13 @@ def save_env_value(key: str, value: str):
               # Sanitize on every read: split concatenated keys, drop stale placeholders
               lines = _sanitize_env_lines(lines)
       
      +    serialized_value = _quote_env_value(value)
      +
           # Find and update or append
           found = False
           for i, line in enumerate(lines):
               if line.strip().startswith(f"{key}="):
      -            lines[i] = f"{key}={value}\n"
      +            lines[i] = f"{key}={serialized_value}\n"
                   found = True
                   break
       
      @@ -6369,7 +6410,7 @@ def save_env_value(key: str, value: str):
               # Ensure there's a newline at the end of the file before appending
               if lines and not lines[-1].endswith("\n"):
                   lines[-1] += "\n"
      -        lines.append(f"{key}={value}\n")
      +        lines.append(f"{key}={serialized_value}\n")
           
           fd, tmp_path = tempfile.mkstemp(dir=str(env_path.parent), suffix='.tmp', prefix='.env_')
           # Preserve original permissions so Docker volume mounts aren't clobbered.
    • tests/hermes_cli/test_config.py
      @@ -354,6 +354,55 @@ def test_save_env_value_preserves_existing_file_mode_on_posix(self, tmp_path):
               env_mode = env_path.stat().st_mode & 0o777
               assert env_mode == 0o640, f"expected 0o640, got {oct(env_mode)}"
       
      +    def test_save_env_value_quotes_values_containing_hash(self, tmp_path):
      +        """Regression test for #30355."""
      +        from dotenv import dotenv_values
      +
      +        with patch.dict(os.environ, {"HERMES_HOME": str(tmp_path)}, clear=False):
      +            os.environ.pop("ANTHROPIC_TOKEN", None)
      +            token = "[redacted:secret-assignment]"
      +            save_env_value("ANTHROPIC_TOKEN", token)
      +
      +            content = (tmp_path / ".env").read_text(encoding="utf-8")
      +            assert f'ANTHROPIC_TOKEN="[redacted:secret-assignment]"' in content
      +
      +            parsed = dotenv_values(str(tmp_path / ".env"))
      +            assert parsed["ANTHROPIC_TOKEN"] == token
      +            assert load_env()["ANTHROPIC_TOKEN"] == token
      +
      +    def test_save_env_value_hash_value_round_trips_quotes_and_backslashes(self, tmp_path):
      +        from dotenv import dotenv_values
      +
      +        with patch.dict(os.environ, {"HERMES_HOME": str(tmp_path)}, clear=False):
      +            os.environ.pop("ANTHROPIC_TOKEN", None)
      +            token = 'abc"def\\ghi#jkl'
      +            save_env_value("ANTHROPIC_TOKEN", token)
      +
      +            content = (tmp_path / ".env").read_text(encoding="utf-8")
      +            assert 'ANTHROPIC_TOKEN="abc\\"def\\\\ghi#jkl"' in content
      +
      +            parsed = dotenv_values(str(tmp_path / ".env"))
      +            assert parsed["ANTHROPIC_TOKEN"] == token
      +            assert load_env()["ANTHROPIC_TOKEN"] == token
      +
      +    def test_save_env_value_updates_hash_value_with_quotes(self, tmp_path):
      +        from dotenv import dotenv_values
      +
      +        with patch.dict(os.environ, {"HERMES_HOME": str(tmp_path)}, clear=False):
      +            os.environ.pop("ANTHROPIC_TOKEN", None)
      +            save_env_value("ANTHROPIC_TOKEN", "old-token")
      +
      +            token = 'abc"def\\ghi#jkl'
      +            save_env_value("ANTHROPIC_TOKEN", token)
      +
      +            content = (tmp_path / ".env").read_text(encoding="utf-8")
      +            assert content.count("ANTHROPIC_TOKEN=") == 1
      +            assert 'ANTHROPIC_TOKEN="abc\\"def\\\\ghi#jkl"' in content
      +
      +            parsed = dotenv_values(str(tmp_path / ".env"))
      +            assert parsed["ANTHROPIC_TOKEN"] == token
      +            assert load_env()["ANTHROPIC_TOKEN"] == token
      +
       
       class TestRemoveEnvValue:
           def test_removes_key_from_env_file(self, tmp_path):

5 PRs closed without merging

From the close comments: superseded by another PR 2 · duplicate 1 · stale or abandoned 1 · other 1

  • #30357 fix(config): quote .env values containing # to prevent truncation (#30355) · stale or abandoned
  • #30473 fix(config): quote dotenv values containing hash · superseded by another PR → #30357, #30355
  • #30724 fix(cli): quote .env values to preserve special characters like # · superseded by another PR → #52653, #33814
  • #30784 fix(cli): quote .env writes, widen Gemini vision allowlist, propagate skills install exit code · other → #30357, #30724, #30712, #30633
  • #33814 fix(config): quote .env values containing # to prevent truncation · duplicate → #30357, #30355, #30747, #30473
Closed issues · 1
  • Closed#30355ANTHROPIC_TOKEN containing # character gets truncated in .env file
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.29: 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
hermes cli
Merged PRs
#52653
Closed issues
1
Attempts
5 PRs closed without merging
Fix link checked
25 Jun 2026

For your agent

The same facts, typed.