diff --git a/hermes_cli/config.py b/hermes_cli/config.py index 750e7c91fdf..a8cb187aa92 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -5719,19 +5719,21 @@ def save_env_value(key: str, value: str): f.flush() os.fsync(f.fileno()) atomic_replace(tmp_path, env_path) - # Restore original permissions before _secure_file may tighten them. + # Preserve the original file mode (e.g. 0640 for Docker volume mounts) + # instead of letting _secure_file unconditionally tighten to 0600. if original_mode is not None: try: os.chmod(env_path, original_mode) except OSError: pass + else: + _secure_file(env_path) except BaseException: try: os.unlink(tmp_path) except OSError: pass raise - _secure_file(env_path) os.environ[key] = value invalidate_env_cache() diff --git a/tests/hermes_cli/test_config.py b/tests/hermes_cli/test_config.py index 29ff5f6f9e9..1a25c1a5d2c 100644 --- a/tests/hermes_cli/test_config.py +++ b/tests/hermes_cli/test_config.py @@ -292,6 +292,25 @@ class TestSaveEnvValueSecure: env_mode = (tmp_path / ".env").stat().st_mode & 0o777 assert env_mode == 0o600 + def test_save_env_value_preserves_existing_file_mode_on_posix(self, tmp_path): + """Regression for #31518: pre-existing .env mode (e.g. 0640 for a + Docker bind-mount that the operator chose) survives subsequent + writes. Previously _secure_file ran unconditionally after the + mode-restore branch and re-tightened to 0600. + """ + if os.name == "nt": + return + + env_path = tmp_path / ".env" + env_path.write_text("EXISTING=value\n") + os.chmod(env_path, 0o640) + + with patch.dict(os.environ, {"HERMES_HOME": str(tmp_path)}): + save_env_value("TENOR_API_KEY", "sk-test-secret") + + env_mode = env_path.stat().st_mode & 0o777 + assert env_mode == 0o640, f"expected 0o640, got {oct(env_mode)}" + class TestRemoveEnvValue: def test_removes_key_from_env_file(self, tmp_path):