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
- 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
Open PRs0
Fixes on record
-
#52653 fix(config): quote .env values containing # to prevent token truncation (#30355)
files:
hermes_cli/config.py,tests/hermes_cli/test_config.pyShow 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.00means 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 LabAt a glance
- Subsystem
- hermes cli
- Merged PRs
- #52653
- Closed issues
- 1
- Attempts
- 5 PRs closed without merging
- Fix link checked
- 25 Jun 2026