# NousResearch/hermes-agent · Quote .env values to prevent hash truncation

## Open now

- 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.

## TL;DR

- 7 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 5 PR(s) closed without merging.
- A fix was merged to main in PR #52653. Verify it addresses your case.
- **Recommended:** read PR #52653 (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-52653`. 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.29`: 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

- #30355: ANTHROPIC_TOKEN containing # character gets truncated in .env file

## Prior & competing PRs

- **#52653: fix(config): quote .env values containing # to prevent token truncation (#30355)**. 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
    @@ -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.
     
    ```

    ```diff
    @@ -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))
    ```

    ```diff
    @@ -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():
    ```

    ```diff
    @@ -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
     
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -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):
    ```
- **#30357: fix(config): quote .env values containing # to prevent truncation (#30355)**. closed without merge
  - maintainer direction: category `stale_abandoned`; implication `active`
- **#30473: fix(config): quote dotenv values containing hash**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#30357`, `#30355`
- **#30724: fix(cli): quote .env values to preserve special characters like #**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#52653`, `#33814`
- **#30784: fix(cli): quote .env writes, widen Gemini vision allowlist, propagate skills install exit code**. closed without merge
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `#30357`, `#30724`, `#30712`, `#30633`
- **#33814: fix(config): quote .env values containing # to prevent truncation**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#30357`, `#30355`, `#30747`, `#30473`

## Fix status

- Merged to main in PR #52653 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-06-25`.
- Subsystem: `hermes_cli`
