fix(config): quote env values containing hash

This commit is contained in:
sweetcornna 2026-06-11 22:37:43 +08:00 committed by kshitijk4poor
parent da73223f4a
commit 150afea942
2 changed files with 93 additions and 3 deletions

View file

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

View file

@ -354,6 +354,55 @@ class TestSaveEnvValueSecure:
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 = "sk-ant-oat01-abc#xyz#more"
save_env_value("ANTHROPIC_TOKEN", token)
content = (tmp_path / ".env").read_text(encoding="utf-8")
assert f'ANTHROPIC_TOKEN="{token}"' 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):