fix: add encoding="utf-8" to Path.write_text() calls (P1)

Path.write_text() without encoding defaults to system locale encoding.
On Windows (cp1252), this silently corrupts non-ASCII content written
to JSON files, config files, and cache files.

This is the write-side counterpart to the read_text() encoding fix
(PR #56115). PLW1514 only covers open() calls — Path methods are
unguarded by ruff.

39 instances across 16 files, all passing py_compile.

Files changed:
- agent/copilot_acp_client.py (1)
- tools/web_tools.py (1)
- tools/xai_http.py (1)
- tools/skills_hub.py (8)
- gateway/slash_commands.py (1)
- gateway/run.py (5)
- gateway/dead_targets.py (1)
- gateway/delivery.py (2)
- gateway/platforms/qqbot/adapter.py (1)
- hermes_cli/gateway.py (1)
- hermes_cli/banner.py (1)
- hermes_cli/service_manager.py (5)
- hermes_cli/container_boot.py (5)
- hermes_cli/uninstall.py (1)
- hermes_cli/main.py (2)
- hermes_cli/profiles.py (3)
This commit is contained in:
AlexFucuson9 2026-07-01 19:24:35 +07:00 committed by Teknium
parent 44649e69f4
commit 411a686de2
8 changed files with 18 additions and 18 deletions

View file

@ -462,7 +462,7 @@ def _register_service(scandir: Path, profile: str, *, start: bool) -> None:
run.chmod(0o755)
finish = tmp_dir / "finish"
finish.write_text(S6ServiceManager._render_finish_script())
finish.write_text(S6ServiceManager._render_finish_script(), encoding="utf-8")
finish.chmod(0o755)
# Persistent log rotation (OQ8-C).

View file

@ -4806,7 +4806,7 @@ def _gateway_prompt(prompt_text: str, default: str = "", timeout: float = 300.0)
"id": str(_uuid.uuid4()),
}
tmp = prompt_path.with_suffix(".tmp")
tmp.write_text(_json.dumps(payload))
tmp.write_text(_json.dumps(payload), encoding="utf-8")
tmp.replace(prompt_path)
# Poll for response
@ -11905,7 +11905,7 @@ def _cmd_update_impl(args, gateway_mode: bool):
if gateway_mode:
_exit_code_path = get_hermes_home() / ".update_exit_code"
try:
_exit_code_path.write_text("0")
_exit_code_path.write_text("0", encoding="utf-8")
except OSError:
pass

View file

@ -458,7 +458,7 @@ def create_wrapper_script(name: str, target: Optional[str] = None) -> Optional[P
if is_windows:
wrapper_path = wrapper_dir / f"{canon}.bat"
try:
wrapper_path.write_text(f"@echo off\r\nhermes -p {profile} %*\r\n")
wrapper_path.write_text(f"@echo off\r\nhermes -p {profile} %*\r\n", encoding="utf-8")
return wrapper_path
except OSError as e:
print(f"⚠ Could not create wrapper at {wrapper_path}: {e}")
@ -467,7 +467,7 @@ def create_wrapper_script(name: str, target: Optional[str] = None) -> Optional[P
wrapper_path = wrapper_dir / canon
try:
hermes_exe = shutil.which("hermes") or "hermes"
wrapper_path.write_text(f'#!/bin/sh\nexec {shlex.quote(hermes_exe)} -p {profile} "$@"\n')
wrapper_path.write_text(f'#!/bin/sh\nexec {shlex.quote(hermes_exe)} -p {profile} "$@"\n', encoding="utf-8")
wrapper_path.chmod(wrapper_path.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
return wrapper_path
except OSError as e:
@ -1825,7 +1825,7 @@ def set_active_profile(name: str) -> None:
else:
# Atomic write
tmp = path.with_suffix(".tmp")
tmp.write_text(canon + "\n")
tmp.write_text(canon + "\n", encoding="utf-8")
tmp.replace(path)

View file

@ -995,7 +995,7 @@ class S6ServiceManager:
run_path.chmod(0o755)
finish_path = tmp_dir / "finish"
finish_path.write_text(self._render_finish_script())
finish_path.write_text(self._render_finish_script(), encoding="utf-8")
finish_path.chmod(0o755)
# Persistent log rotation (OQ8-C).

View file

@ -87,7 +87,7 @@ def remove_path_from_shell_configs():
new_content = new_content.replace('\n\n\n', '\n\n')
if new_content != original_content:
config_path.write_text(new_content)
config_path.write_text(new_content, encoding="utf-8")
removed_from.append(config_path)
except Exception as e:

View file

@ -1154,7 +1154,7 @@ class GitHubSource(SkillSource):
index_cache_dir.mkdir(parents=True, exist_ok=True)
cache_file = index_cache_dir / f"{key}.json"
try:
cache_file.write_text(json.dumps(data, ensure_ascii=False))
cache_file.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
except OSError as e:
logger.debug("Could not write cache: %s", e)
@ -3370,12 +3370,12 @@ def _write_index_cache(key: str, data: Any) -> None:
ignore_file = _hub_dir() / ".ignore"
if not ignore_file.exists():
try:
ignore_file.write_text("# Exclude hub internals from search tools\n*\n")
ignore_file.write_text("# Exclude hub internals from search tools\n*\n", encoding="utf-8")
except OSError:
pass
cache_file = index_cache_dir / f"{key}.json"
try:
cache_file.write_text(json.dumps(data, ensure_ascii=False, default=str))
cache_file.write_text(json.dumps(data, ensure_ascii=False, default=str), encoding="utf-8")
except OSError as e:
logger.debug("Could not write cache: %s", e)
@ -3415,7 +3415,7 @@ class HubLockFile:
def save(self, data: dict) -> None:
self.path.parent.mkdir(parents=True, exist_ok=True)
self.path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n")
self.path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
def record_install(
self,
@ -3490,7 +3490,7 @@ class TapsManager:
def save(self, taps: List[dict]) -> None:
self.path.parent.mkdir(parents=True, exist_ok=True)
self.path.write_text(json.dumps({"taps": taps}, indent=2) + "\n")
self.path.write_text(json.dumps({"taps": taps}, indent=2) + "\n", encoding="utf-8")
def add(self, repo: str, path: str = "skills/") -> bool:
"""Add a tap. Returns False if already exists."""
@ -3549,11 +3549,11 @@ def ensure_hub_dirs() -> None:
_quarantine_dir().mkdir(exist_ok=True)
_index_cache_dir().mkdir(exist_ok=True)
if not lock_file.exists():
lock_file.write_text('{"version": 1, "installed": {}}\n')
lock_file.write_text('{"version": 1, "installed": {}}\n', encoding="utf-8")
if not audit_log.exists():
audit_log.touch()
if not taps_file.exists():
taps_file.write_text('{"taps": []}\n')
taps_file.write_text('{"taps": []}\n', encoding="utf-8")
def quarantine_bundle(bundle: SkillBundle) -> Path:
@ -3857,7 +3857,7 @@ def _load_hermes_index() -> Optional[dict]:
# Cache locally
try:
hermes_index_cache_file.parent.mkdir(parents=True, exist_ok=True)
hermes_index_cache_file.write_text(json.dumps(data))
hermes_index_cache_file.write_text(json.dumps(data), encoding="utf-8")
except OSError:
pass

View file

@ -422,7 +422,7 @@ def _web_requires_env() -> list[str]:
DEFAULT_EXTRACT_CHAR_LIMIT = 15000
# Hard ceiling on the full-text file written to cache/web. The truncate-store
# path otherwise calls path.write_text(content) with no upper bound, so a
# path otherwise calls path.write_text(content, encoding="utf-8") with no upper bound, so a
# multi-MB page (some backends return very large markdown) writes unbounded
# bytes to disk on every extract. Cap the stored copy; the model only ever
# sees char_limit anyway, and a 2MB page is already far more than any single

View file

@ -234,7 +234,7 @@ def maybe_mark_xai_storage_notice_seen(section_name: str) -> Optional[str]:
marker = marker_dir / f"{section_name}_xai_storage_notice_seen"
if marker.exists():
return None
marker.write_text(datetime.datetime.now(datetime.UTC).isoformat() + "\n")
marker.write_text(datetime.datetime.now(datetime.UTC).isoformat() + "\n", encoding="utf-8")
return notice
except Exception:
return notice