From cf3236ed279327ba3f8163e1e96281400adc5b82 Mon Sep 17 00:00:00 2001 From: Dean Kerr Date: Thu, 26 Feb 2026 18:37:20 +1100 Subject: [PATCH 1/4] fix: resolve .env path from ~/.hermes/ in cli.py, matching run_agent.py pattern Load ~/.hermes/.env first with project root as dev fallback, and remove redundant second load_dotenv call inside load_cli_config(). Also sets MSWEA_GLOBAL_CONFIG_DIR so mini-swe-agent shares the same config. Co-Authored-By: Claude Opus 4.6 --- cli.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/cli.py b/cli.py index 56c40ad9c..19ab53bbb 100755 --- a/cli.py +++ b/cli.py @@ -49,16 +49,26 @@ import threading import queue -# Load environment variables first +# Load .env from ~/.hermes/.env first, then project root as dev fallback from dotenv import load_dotenv from hermes_constants import OPENROUTER_BASE_URL -env_path = Path(__file__).parent / '.env' -if env_path.exists(): +_hermes_home = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes")) +_user_env = _hermes_home / ".env" +_project_env = Path(__file__).parent / '.env' +if _user_env.exists(): try: - load_dotenv(dotenv_path=env_path, encoding="utf-8") + load_dotenv(dotenv_path=_user_env, encoding="utf-8") except UnicodeDecodeError: - load_dotenv(dotenv_path=env_path, encoding="latin-1") + load_dotenv(dotenv_path=_user_env, encoding="latin-1") +elif _project_env.exists(): + try: + load_dotenv(dotenv_path=_project_env, encoding="utf-8") + except UnicodeDecodeError: + load_dotenv(dotenv_path=_project_env, encoding="latin-1") + +# Point mini-swe-agent at ~/.hermes/ so it shares our config +os.environ.setdefault("MSWEA_GLOBAL_CONFIG_DIR", str(_hermes_home)) # ============================================================================= # Configuration Loading @@ -132,15 +142,6 @@ def load_cli_config() -> Dict[str, Any]: else: config_path = project_config_path - # Also load .env from ~/.hermes/.env if it exists - user_env_path = Path.home() / '.hermes' / '.env' - if user_env_path.exists(): - from dotenv import load_dotenv - try: - load_dotenv(dotenv_path=user_env_path, override=True, encoding="utf-8") - except UnicodeDecodeError: - load_dotenv(dotenv_path=user_env_path, override=True, encoding="latin-1") - # Default configuration defaults = { "model": { From f2891b70d02628e1c334353127edce976f4a20b5 Mon Sep 17 00:00:00 2001 From: Dean Kerr Date: Thu, 26 Feb 2026 18:51:46 +1100 Subject: [PATCH 2/4] fix: respect HERMES_HOME env var in gateway and cron scheduler Both entry points hardcoded Path.home() / ".hermes" for .env, config.yaml, logs, and lock files. Now uses _hermes_home which reads HERMES_HOME env var with ~/.hermes as default, matching cli.py and run_agent.py. Co-Authored-By: Claude Opus 4.6 --- cron/scheduler.py | 11 +++++++---- gateway/run.py | 25 ++++++++++++++----------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/cron/scheduler.py b/cron/scheduler.py index 64badee9c..23cf5cd61 100644 --- a/cron/scheduler.py +++ b/cron/scheduler.py @@ -34,8 +34,11 @@ sys.path.insert(0, str(Path(__file__).parent.parent)) from cron.jobs import get_due_jobs, mark_job_run, save_job_output +# Resolve Hermes home directory (respects HERMES_HOME override) +_hermes_home = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes")) + # File-based lock prevents concurrent ticks from gateway + daemon + systemd timer -_LOCK_DIR = Path.home() / ".hermes" / "cron" +_LOCK_DIR = _hermes_home / "cron" _LOCK_FILE = _LOCK_DIR / ".tick.lock" @@ -165,9 +168,9 @@ def run_job(job: dict) -> tuple[bool, str, str, Optional[str]]: # changes take effect without a gateway restart. from dotenv import load_dotenv try: - load_dotenv(os.path.expanduser("~/.hermes/.env"), override=True, encoding="utf-8") + load_dotenv(str(_hermes_home / ".env"), override=True, encoding="utf-8") except UnicodeDecodeError: - load_dotenv(os.path.expanduser("~/.hermes/.env"), override=True, encoding="latin-1") + load_dotenv(str(_hermes_home / ".env"), override=True, encoding="latin-1") model = os.getenv("HERMES_MODEL", "anthropic/claude-opus-4.6") # Custom endpoint (OPENAI_*) takes precedence, matching CLI behavior @@ -176,7 +179,7 @@ def run_job(job: dict) -> tuple[bool, str, str, Optional[str]]: try: import yaml - _cfg_path = os.path.expanduser("~/.hermes/config.yaml") + _cfg_path = str(_hermes_home / "config.yaml") if os.path.exists(_cfg_path): with open(_cfg_path) as _f: _cfg = yaml.safe_load(_f) or {} diff --git a/gateway/run.py b/gateway/run.py index e332991de..030c10987 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -28,9 +28,12 @@ from typing import Dict, Optional, Any, List # Add parent directory to path sys.path.insert(0, str(Path(__file__).parent.parent)) +# Resolve Hermes home directory (respects HERMES_HOME override) +_hermes_home = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes")) + # Load environment variables from ~/.hermes/.env first from dotenv import load_dotenv -_env_path = Path.home() / '.hermes' / '.env' +_env_path = _hermes_home / '.env' if _env_path.exists(): try: load_dotenv(_env_path, encoding="utf-8") @@ -41,7 +44,7 @@ load_dotenv() # Bridge config.yaml values into the environment so os.getenv() picks them up. # Values already set in the environment (from .env or shell) take precedence. -_config_path = Path.home() / '.hermes' / 'config.yaml' +_config_path = _hermes_home / 'config.yaml' if _config_path.exists(): try: import yaml as _yaml @@ -141,7 +144,7 @@ class GatewayRunner: if not file_path: try: import yaml as _y - cfg_path = Path.home() / ".hermes" / "config.yaml" + cfg_path = _hermes_home / "config.yaml" if cfg_path.exists(): with open(cfg_path) as _f: cfg = _y.safe_load(_f) or {} @@ -152,7 +155,7 @@ class GatewayRunner: return [] path = Path(file_path).expanduser() if not path.is_absolute(): - path = Path.home() / ".hermes" / path + path = _hermes_home / path if not path.exists(): logger.warning("Prefill messages file not found: %s", path) return [] @@ -179,7 +182,7 @@ class GatewayRunner: return prompt try: import yaml as _y - cfg_path = Path.home() / ".hermes" / "config.yaml" + cfg_path = _hermes_home / "config.yaml" if cfg_path.exists(): with open(cfg_path) as _f: cfg = _y.safe_load(_f) or {} @@ -200,7 +203,7 @@ class GatewayRunner: if not effort: try: import yaml as _y - cfg_path = Path.home() / ".hermes" / "config.yaml" + cfg_path = _hermes_home / "config.yaml" if cfg_path.exists(): with open(cfg_path) as _f: cfg = _y.safe_load(_f) or {} @@ -884,7 +887,7 @@ class GatewayRunner: try: import yaml - config_path = Path.home() / '.hermes' / 'config.yaml' + config_path = _hermes_home / 'config.yaml' if config_path.exists(): with open(config_path, 'r') as f: config = yaml.safe_load(f) or {} @@ -981,7 +984,7 @@ class GatewayRunner: # Save to config.yaml try: import yaml - config_path = Path.home() / '.hermes' / 'config.yaml' + config_path = _hermes_home / 'config.yaml' user_config = {} if config_path.exists(): with open(config_path) as f: @@ -1243,7 +1246,7 @@ class GatewayRunner: # Try to load platform_toolsets from config platform_toolsets_config = {} try: - config_path = Path.home() / '.hermes' / 'config.yaml' + config_path = _hermes_home / 'config.yaml' if config_path.exists(): import yaml with open(config_path, 'r') as f: @@ -1405,7 +1408,7 @@ class GatewayRunner: try: import yaml as _y - _cfg_path = Path.home() / ".hermes" / "config.yaml" + _cfg_path = _hermes_home / "config.yaml" if _cfg_path.exists(): with open(_cfg_path) as _f: _cfg = _y.safe_load(_f) or {} @@ -1697,7 +1700,7 @@ async def start_gateway(config: Optional[GatewayConfig] = None) -> bool: A False return causes a non-zero exit code so systemd can auto-restart. """ # Configure rotating file log so gateway output is persisted for debugging - log_dir = Path.home() / '.hermes' / 'logs' + log_dir = _hermes_home / 'logs' log_dir.mkdir(parents=True, exist_ok=True) file_handler = RotatingFileHandler( log_dir / 'gateway.log', From 696e2316a861868af6106c7a8d4caf2d82797b0a Mon Sep 17 00:00:00 2001 From: Dean Kerr Date: Thu, 26 Feb 2026 19:01:13 +1100 Subject: [PATCH 3/4] fix: respect HERMES_HOME and add encoding fallback in rl_cli.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consistent with other entry points: use _hermes_home from HERMES_HOME env var, and add UTF-8 → latin-1 encoding fallback on load_dotenv. Co-Authored-By: Claude Opus 4.6 --- rl_cli.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/rl_cli.py b/rl_cli.py index eaeec1d96..3aa0412d4 100644 --- a/rl_cli.py +++ b/rl_cli.py @@ -27,19 +27,25 @@ from pathlib import Path import fire import yaml -# Load environment variables from .env file +# Load .env from ~/.hermes/.env first, then project root as dev fallback from dotenv import load_dotenv -# Load from ~/.hermes/.env first, then local .env -hermes_env_path = Path.home() / '.hermes' / '.env' -local_env_path = Path(__file__).parent / '.env' +_hermes_home = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes")) +_user_env = _hermes_home / ".env" +_project_env = Path(__file__).parent / '.env' -if hermes_env_path.exists(): - load_dotenv(dotenv_path=hermes_env_path) - print(f"✅ Loaded environment variables from {hermes_env_path}") -elif local_env_path.exists(): - load_dotenv(dotenv_path=local_env_path) - print(f"✅ Loaded environment variables from {local_env_path}") +if _user_env.exists(): + try: + load_dotenv(dotenv_path=_user_env, encoding="utf-8") + except UnicodeDecodeError: + load_dotenv(dotenv_path=_user_env, encoding="latin-1") + print(f"✅ Loaded environment variables from {_user_env}") +elif _project_env.exists(): + try: + load_dotenv(dotenv_path=_project_env, encoding="utf-8") + except UnicodeDecodeError: + load_dotenv(dotenv_path=_project_env, encoding="latin-1") + print(f"✅ Loaded environment variables from {_project_env}") # Set terminal working directory to tinker-atropos submodule # This ensures terminal commands run in the right context for RL work @@ -77,7 +83,7 @@ def load_hermes_config() -> dict: Returns: dict: Configuration with model, base_url, etc. """ - config_path = Path.home() / '.hermes' / 'config.yaml' + config_path = _hermes_home / 'config.yaml' config = { "model": DEFAULT_MODEL, From 9dc5615b9d86517f8d5ca2face5d94d5357dbc49 Mon Sep 17 00:00:00 2001 From: Dean Kerr Date: Thu, 26 Feb 2026 19:20:30 +1100 Subject: [PATCH 4/4] fix: use HERMES_HOME constant in doctor.py directory check Line 184 hardcoded Path.home() / ".hermes" instead of using the existing HERMES_HOME variable which already respects the env var. Co-Authored-By: Claude Opus 4.6 --- hermes_cli/doctor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hermes_cli/doctor.py b/hermes_cli/doctor.py index c799a74c6..e68b98bf7 100644 --- a/hermes_cli/doctor.py +++ b/hermes_cli/doctor.py @@ -181,7 +181,7 @@ def run_doctor(args): print() print(color("◆ Directory Structure", Colors.CYAN, Colors.BOLD)) - hermes_home = Path.home() / ".hermes" + hermes_home = HERMES_HOME if hermes_home.exists(): check_ok("~/.hermes directory exists") else: