From 3667138d05da6787ce7bb9e353fe8d74ecb36fd9 Mon Sep 17 00:00:00 2001 From: alireza78a Date: Wed, 11 Mar 2026 08:58:33 -0700 Subject: [PATCH] fix(config): atomic write for .env to prevent API key loss on crash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit save_env_value() used bare open('w') which truncates .env immediately. A crash or OOM kill between truncation and completed write silently wipes every credential in the file. Write now goes to a temp file first, then os.replace() swaps it atomically. Either the old .env exists or the new one does — never a truncated half-write. Same pattern used in cron/jobs.py. Cherry-picked from PR #842 by alireza78a, rebased onto current main with conflict resolution (_secure_file refactor). Co-authored-by: alireza78a --- hermes_cli/config.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/hermes_cli/config.py b/hermes_cli/config.py index 758118492..f2b5d42c1 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -17,6 +17,7 @@ import stat import subprocess import sys +import tempfile from pathlib import Path from typing import Dict, Any, Optional, List, Tuple @@ -958,8 +959,19 @@ def save_env_value(key: str, value: str): lines[-1] += "\n" lines.append(f"{key}={value}\n") - with open(env_path, 'w', **write_kw) as f: - f.writelines(lines) + fd, tmp_path = tempfile.mkstemp(dir=str(env_path.parent), suffix='.tmp', prefix='.env_') + try: + with os.fdopen(fd, 'w', **write_kw) as f: + f.writelines(lines) + f.flush() + os.fsync(f.fileno()) + os.replace(tmp_path, env_path) + except BaseException: + try: + os.unlink(tmp_path) + except OSError: + pass + raise _secure_file(env_path) # Restrict .env permissions to owner-only (contains API keys)