fix(auth): read .env as utf-8-sig in the dotenv-vs-shell detector

_remove_env_source() decides whether a credential var lives in ~/.hermes/.env
or the shell by scanning the .env with env_path.read_text(errors="replace") —
no encoding. read_text() with no encoding falls back to the system locale
(cp1252/GBK on Windows) and never strips a BOM.

The canonical .env readers in hermes_cli/config.py all use
encoding="utf-8-sig" precisely because 'users may edit .env in Notepad which
adds one' (a BOM), and doctor.py documents that .env is written as UTF-8
everywhere. This sibling reader diverged: on a Notepad-edited .env the BOM
prefixes the first line, so line.strip().startswith(f"{env_var}=") is False
for the first variable — the detector reports a .env-backed key as a phantom
shell export and prints a misleading 'still set in your shell environment'
hint on .

Match the canonical reader (utf-8-sig + errors=replace). Adds a regression
test with a BOM'd .env.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
solyanviktor-star
2026-07-08 15:29:05 +03:00
committed by Teknium
parent 3fee5c291b
commit 7fef76a6cf
2 changed files with 50 additions and 1 deletions

View File

@@ -162,9 +162,17 @@ def _remove_env_source(provider: str, removed) -> RemovalResult:
try:
env_path = get_env_path()
if env_path.exists():
# Read the .env as UTF-8 with BOM tolerance, matching the
# canonical reader in hermes_cli/config.py. read_text() with no
# encoding falls back to the system locale (cp1252/GBK on Windows)
# and never strips a BOM, so a Notepad-edited .env (BOM + non-ASCII
# values) would make the first line fail the startswith() check —
# misreporting a .env-backed var as a shell export.
env_in_dotenv = any(
line.strip().startswith(f"{env_var}=")
for line in env_path.read_text(errors="replace", encoding="utf-8").splitlines()
for line in env_path.read_text(
encoding="utf-8-sig", errors="replace"
).splitlines()
)
except OSError:
pass

View File

@@ -769,3 +769,44 @@ def test_auth_remove_copilot_suppresses_all_variants(tmp_path, monkeypatch):
assert is_source_suppressed("copilot", "env:GITHUB_TOKEN")
def test_auth_remove_env_seeded_dotenv_with_bom_no_shell_hint(tmp_path, monkeypatch, capsys):
"""A Notepad-edited .env carries a UTF-8 BOM. The dotenv-vs-shell
detector must still see the first variable as living in .env (and not
warn about a phantom shell export). Regression for the reader that
dropped encoding='utf-8-sig' and misread the BOM'd first line.
"""
hermes_home = tmp_path / "hermes"
hermes_home.mkdir(parents=True, exist_ok=True)
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
monkeypatch.delenv("DEEPSEEK_API_KEY", raising=False)
# BOM prefix (utf-8-sig) + the target var as the FIRST line.
(hermes_home / ".env").write_bytes(
b"\xef\xbb\xbfDEEPSEEK_API_KEY=sk-ds-only\n"
)
monkeypatch.setenv("DEEPSEEK_API_KEY", "sk-ds-only")
_write_auth_store(
tmp_path,
{
"version": 1,
"credential_pool": {
"deepseek": [{
"id": "env-1",
"label": "DEEPSEEK_API_KEY",
"auth_type": "api_key",
"priority": 0,
"source": "env:DEEPSEEK_API_KEY",
"access_token": "sk-ds-only",
}]
},
},
)
from types import SimpleNamespace
from hermes_cli.auth_commands import auth_remove_command
auth_remove_command(SimpleNamespace(provider="deepseek", target="1"))
out = capsys.readouterr().out
assert "Cleared DEEPSEEK_API_KEY from .env" in out
assert "still set in your shell environment" not in out