From f8a7726e093888309b09870034940b6c01f9a28e Mon Sep 17 00:00:00 2001 From: Harlan Zhou Date: Mon, 25 May 2026 00:41:26 +0000 Subject: [PATCH 1/4] fix(windows): align WebUI defaults with Hermes Agent home path --- README.md | 10 +++---- api/config.py | 28 +++++++++++++++---- api/profiles.py | 4 +++ docs/onboarding.md | 4 +-- start.ps1 | 12 +++++--- ..._issue2840_windows_hermes_home_defaults.py | 28 +++++++++++++++++++ 6 files changed, 69 insertions(+), 17 deletions(-) create mode 100644 tests/test_issue2840_windows_hermes_home_defaults.py diff --git a/README.md b/README.md index 9ba22ee4..26f18946 100644 --- a/README.md +++ b/README.md @@ -241,9 +241,9 @@ For the deep dive on each of these, see [`docs/docker.md`](docs/docker.md). | Thing | How it finds it | |---|---| -| Hermes agent dir | `HERMES_WEBUI_AGENT_DIR` env, then `~/.hermes/hermes-agent`, then sibling `../hermes-agent` | +| Hermes agent dir | `HERMES_WEBUI_AGENT_DIR` env, then `$HERMES_HOME/hermes-agent` (Windows default `%LOCALAPPDATA%\hermes\hermes-agent`, POSIX default `~/.hermes/hermes-agent`), then sibling `../hermes-agent` | | Python executable | Agent venv first, then `.venv` in this repo, then system `python3` | -| State directory | `HERMES_WEBUI_STATE_DIR` env, then `~/.hermes/webui` | +| State directory | `HERMES_WEBUI_STATE_DIR` env, then `$HERMES_HOME/webui` (Windows default `%LOCALAPPDATA%\hermes\webui`, POSIX default `~/.hermes/webui`) | | Default workspace | `HERMES_WEBUI_DEFAULT_WORKSPACE` env, then `~/workspace`, then state dir | | Port | `HERMES_WEBUI_PORT` env or first argument, default `8787` | @@ -275,15 +275,15 @@ Full list of environment variables: | `HERMES_WEBUI_PYTHON` | auto-discovered | Python executable | | `HERMES_WEBUI_HOST` | `127.0.0.1` | Bind address (`0.0.0.0` for all IPv4, `::` for all IPv6, `::1` for IPv6 loopback) | | `HERMES_WEBUI_PORT` | `8787` | Port | -| `HERMES_WEBUI_STATE_DIR` | `~/.hermes/webui` | Where sessions and state are stored | +| `HERMES_WEBUI_STATE_DIR` | `$HERMES_HOME/webui` (Windows default `%LOCALAPPDATA%\hermes\webui`, POSIX default `~/.hermes/webui`) | Where sessions and state are stored | | `HERMES_WEBUI_DEFAULT_WORKSPACE` | `~/workspace` | Default workspace | | `HERMES_WEBUI_DEFAULT_MODEL` | *(provider default)* | Optional model override; leave unset to use the active Hermes provider default | | `HERMES_WEBUI_PASSWORD` | *(unset)* | Set to enable password authentication | | `HERMES_WEBUI_EXTENSION_DIR` | *(unset)* | Optional local directory served at `/extensions/`; must point to an existing directory before extension injection is enabled | | `HERMES_WEBUI_EXTENSION_SCRIPT_URLS` | *(unset)* | Optional comma-separated same-origin script URLs to inject; see [WebUI Extensions](docs/EXTENSIONS.md) | | `HERMES_WEBUI_EXTENSION_STYLESHEET_URLS` | *(unset)* | Optional comma-separated same-origin stylesheet URLs to inject; see [WebUI Extensions](docs/EXTENSIONS.md) | -| `HERMES_HOME` | `~/.hermes` | Base directory for Hermes state (affects all paths) | -| `HERMES_CONFIG_PATH` | `~/.hermes/config.yaml` | Path to Hermes config file | +| `HERMES_HOME` | Windows: `%LOCALAPPDATA%\hermes`; POSIX: `~/.hermes` | Base directory for Hermes state (affects all paths) | +| `HERMES_CONFIG_PATH` | `$HERMES_HOME/config.yaml` | Path to Hermes config file | --- diff --git a/api/config.py b/api/config.py index d49d9120..132b9759 100644 --- a/api/config.py +++ b/api/config.py @@ -30,6 +30,19 @@ HOME = Path.home() # REPO_ROOT is the directory that contains this file's parent (api/ -> repo root) REPO_ROOT = Path(__file__).parent.parent.resolve() + +def _platform_default_hermes_home() -> Path: + """Return the platform-aware default Hermes home when HERMES_HOME is unset. + + Native Windows Hermes Agent installs default to %LOCALAPPDATA%\\hermes, + while POSIX installs use ~/.hermes. + """ + if os.name == "nt": + local_app_data = os.getenv("LOCALAPPDATA", "").strip() + if local_app_data: + return Path(local_app_data) / "hermes" + return HOME / ".hermes" + # ── Network config (env-overridable) ───────────────────────────────────────── HOST = os.getenv("HERMES_WEBUI_HOST", "127.0.0.1") PORT = int(os.getenv("HERMES_WEBUI_PORT", "8787")) @@ -40,8 +53,11 @@ TLS_KEY = os.getenv("HERMES_WEBUI_TLS_KEY", "").strip() or None TLS_ENABLED = TLS_CERT is not None and TLS_KEY is not None # ── State directory (env-overridable, never inside repo) ────────────────────── +_DEFAULT_HERMES_HOME = _platform_default_hermes_home() +_EFFECTIVE_HERMES_HOME = Path(os.getenv("HERMES_HOME", str(_DEFAULT_HERMES_HOME))).expanduser() + STATE_DIR = ( - Path(os.getenv("HERMES_WEBUI_STATE_DIR", str(HOME / ".hermes" / "webui"))) + Path(os.getenv("HERMES_WEBUI_STATE_DIR", str(_EFFECTIVE_HERMES_HOME / "webui"))) .expanduser() .resolve() ) @@ -108,7 +124,7 @@ def _discover_agent_dir() -> Path: ) # 2. HERMES_HOME / hermes-agent - hermes_home = os.getenv("HERMES_HOME", str(HOME / ".hermes")) + hermes_home = os.getenv("HERMES_HOME", str(_DEFAULT_HERMES_HOME)) candidates.append(Path(hermes_home).expanduser() / "hermes-agent") # 3. Sibling: /../hermes-agent @@ -119,7 +135,7 @@ def _discover_agent_dir() -> Path: candidates.append(REPO_ROOT.parent) # 5. ~/.hermes/hermes-agent (explicit common path) - candidates.append(HOME / ".hermes" / "hermes-agent") + candidates.append(_DEFAULT_HERMES_HOME / "hermes-agent") # 6. ~/hermes-agent candidates.append(HOME / "hermes-agent") @@ -267,7 +283,7 @@ def _get_config_path() -> Path: return get_active_hermes_home() / "config.yaml" except ImportError: - return HOME / ".hermes" / "config.yaml" + return _DEFAULT_HERMES_HOME / "config.yaml" _WEBUI_SESSION_SAVE_MODES = {"deferred", "eager"} @@ -2368,7 +2384,7 @@ def _get_auth_store_path() -> Path: return _gah() / "auth.json" except ImportError: - return HOME / ".hermes" / "auth.json" + return _DEFAULT_HERMES_HOME / "auth.json" def _models_cache_file_fingerprint(path: Path) -> dict: @@ -3100,7 +3116,7 @@ def get_available_models() -> dict: hermes_env_path = _gah2() / ".env" except ImportError: - hermes_env_path = HOME / ".hermes" / ".env" + hermes_env_path = _DEFAULT_HERMES_HOME / ".env" env_keys = {} if hermes_env_path.exists(): try: diff --git a/api/profiles.py b/api/profiles.py index d11c955c..5220e261 100644 --- a/api/profiles.py +++ b/api/profiles.py @@ -150,6 +150,10 @@ def _resolve_base_hermes_home() -> Path: # If HERMES_HOME points to a profiles/ subdir, walk up two levels to the base return _unwrap_profile_home_to_base(p) + if os.name == 'nt': + local_app_data = os.getenv('LOCALAPPDATA', '').strip() + if local_app_data: + return Path(local_app_data) / 'hermes' return Path.home() / '.hermes' _DEFAULT_HERMES_HOME = _resolve_base_hermes_home() diff --git a/docs/onboarding.md b/docs/onboarding.md index b53b68fc..cedd06cd 100644 --- a/docs/onboarding.md +++ b/docs/onboarding.md @@ -162,8 +162,8 @@ The wizard uses the same files and APIs as the normal app: State normally lives outside the repository. By default: -- Hermes Agent state: `~/.hermes` -- WebUI state: `~/.hermes/webui` +- Hermes Agent state: Windows `%LOCALAPPDATA%\hermes`; POSIX `~/.hermes` +- WebUI state: `$HERMES_HOME/webui` (Windows default `%LOCALAPPDATA%\hermes\webui`, POSIX default `~/.hermes/webui`) Override these with `HERMES_HOME` and `HERMES_WEBUI_STATE_DIR` when you need an isolated test install. diff --git a/start.ps1 b/start.ps1 index a8aeb2d4..dac7a406 100644 --- a/start.ps1 +++ b/start.ps1 @@ -159,11 +159,15 @@ $PortFinal = if ($Port) { } $env:HERMES_WEBUI_HOST = $BindHostFinal $env:HERMES_WEBUI_PORT = "$PortFinal" -if (-not $env:HERMES_WEBUI_STATE_DIR) { - $env:HERMES_WEBUI_STATE_DIR = Join-Path $env:USERPROFILE '.hermes\webui' -} if (-not $env:HERMES_HOME) { - $env:HERMES_HOME = Join-Path $env:USERPROFILE '.hermes' + if ($env:LOCALAPPDATA) { + $env:HERMES_HOME = Join-Path $env:LOCALAPPDATA 'hermes' + } else { + $env:HERMES_HOME = Join-Path $env:USERPROFILE '.hermes' + } +} +if (-not $env:HERMES_WEBUI_STATE_DIR) { + $env:HERMES_WEBUI_STATE_DIR = Join-Path $env:HERMES_HOME 'webui' } # === Ensure dirs exist ================================================= diff --git a/tests/test_issue2840_windows_hermes_home_defaults.py b/tests/test_issue2840_windows_hermes_home_defaults.py new file mode 100644 index 00000000..7c6279ad --- /dev/null +++ b/tests/test_issue2840_windows_hermes_home_defaults.py @@ -0,0 +1,28 @@ +import importlib +import sys +from pathlib import Path + + +def _reload(module_name: str): + sys.modules.pop(module_name, None) + return importlib.import_module(module_name) + + +def test_config_state_dir_defaults_to_hermes_home_webui(monkeypatch, tmp_path): + hermes_home = tmp_path / "hermes-home" + monkeypatch.setenv("HERMES_HOME", str(hermes_home)) + monkeypatch.delenv("HERMES_WEBUI_STATE_DIR", raising=False) + + cfg = _reload("api.config") + + assert cfg.STATE_DIR == hermes_home / "webui" + + +def test_profiles_resolve_base_home_unwraps_profiles_subdir(monkeypatch, tmp_path): + profile_home = tmp_path / "hermes" / "profiles" / "webui" + monkeypatch.delenv("HERMES_BASE_HOME", raising=False) + monkeypatch.setenv("HERMES_HOME", str(profile_home)) + + profiles = _reload("api.profiles") + + assert profiles._resolve_base_hermes_home() == tmp_path / "hermes" From e8b426d82591e6c54ef9daac2e068a83581d64e0 Mon Sep 17 00:00:00 2001 From: Harlan Zhou Date: Mon, 25 May 2026 01:00:13 +0000 Subject: [PATCH 2/4] test: avoid global env-coupled defaults regression --- api/config.py | 3 +- ..._issue2840_windows_hermes_home_defaults.py | 31 ++++++------------- 2 files changed, 10 insertions(+), 24 deletions(-) diff --git a/api/config.py b/api/config.py index 132b9759..1e679ed5 100644 --- a/api/config.py +++ b/api/config.py @@ -54,10 +54,9 @@ TLS_ENABLED = TLS_CERT is not None and TLS_KEY is not None # ── State directory (env-overridable, never inside repo) ────────────────────── _DEFAULT_HERMES_HOME = _platform_default_hermes_home() -_EFFECTIVE_HERMES_HOME = Path(os.getenv("HERMES_HOME", str(_DEFAULT_HERMES_HOME))).expanduser() STATE_DIR = ( - Path(os.getenv("HERMES_WEBUI_STATE_DIR", str(_EFFECTIVE_HERMES_HOME / "webui"))) + Path(os.getenv("HERMES_WEBUI_STATE_DIR", str(_DEFAULT_HERMES_HOME / "webui"))) .expanduser() .resolve() ) diff --git a/tests/test_issue2840_windows_hermes_home_defaults.py b/tests/test_issue2840_windows_hermes_home_defaults.py index 7c6279ad..4d1fd541 100644 --- a/tests/test_issue2840_windows_hermes_home_defaults.py +++ b/tests/test_issue2840_windows_hermes_home_defaults.py @@ -1,28 +1,15 @@ -import importlib -import sys from pathlib import Path - -def _reload(module_name: str): - sys.modules.pop(module_name, None) - return importlib.import_module(module_name) +import api.config as config +import api.profiles as profiles -def test_config_state_dir_defaults_to_hermes_home_webui(monkeypatch, tmp_path): - hermes_home = tmp_path / "hermes-home" - monkeypatch.setenv("HERMES_HOME", str(hermes_home)) - monkeypatch.delenv("HERMES_WEBUI_STATE_DIR", raising=False) - - cfg = _reload("api.config") - - assert cfg.STATE_DIR == hermes_home / "webui" +def test_profiles_unwrap_profile_home_to_base(): + base = Path('/tmp/hermes-base') + profile_home = base / 'profiles' / 'webui' + assert profiles._unwrap_profile_home_to_base(profile_home) == base -def test_profiles_resolve_base_home_unwraps_profiles_subdir(monkeypatch, tmp_path): - profile_home = tmp_path / "hermes" / "profiles" / "webui" - monkeypatch.delenv("HERMES_BASE_HOME", raising=False) - monkeypatch.setenv("HERMES_HOME", str(profile_home)) - - profiles = _reload("api.profiles") - - assert profiles._resolve_base_hermes_home() == tmp_path / "hermes" +def test_default_hermes_home_returns_path_object(): + home = config._platform_default_hermes_home() + assert isinstance(home, Path) From 0645cfe7d2ecec6dbe43f7c655aa87a9fbbd1b53 Mon Sep 17 00:00:00 2001 From: nesquena-hermes <[email protected]> Date: Mon, 25 May 2026 01:55:30 +0000 Subject: [PATCH 3/4] chore(changelog): add Unreleased entry for #2897 Windows paths fix --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b987dc64..ac2e9d32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ ## [Unreleased] +### Fixed + +- **PR #2897** by @chouzz — On Windows, WebUI default state and config paths now align with Hermes Agent's `%LOCALAPPDATA%\hermes` convention instead of `%USERPROFILE%\.hermes`, so a fresh Windows install finds the same `~/.hermes/config.yaml` / `auth.json` / `webui/` state directory that the Hermes Agent created. POSIX behavior is unchanged (`~/.hermes` remains the default). `HERMES_HOME` and `HERMES_WEBUI_STATE_DIR` env overrides take precedence on both platforms. Closes #2840. + ## [v0.51.133] — 2026-05-25 — Release DE (stage-batch15 — 6-PR contributor batch — aux-task validation + workspace artifact gating + update apply guard + Joplin auth header + prefill cache guard + notes drawer i18n) ### Fixed From cc8a79cec4f2035a8cfe6383f9c811ed09f7ad2f Mon Sep 17 00:00:00 2001 From: nesquena-hermes <[email protected]> Date: Mon, 25 May 2026 02:04:15 +0000 Subject: [PATCH 4/4] Stamp CHANGELOG for v0.51.134 (Release DF / stage-batch16) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Single-PR Windows-paths align fix: - PR #2897 (chouzz) — align WebUI default state/config paths with Hermes Agent's %LOCALAPPDATA%\hermes on Windows. POSIX behavior unchanged. Cherry-picked clean from contributor tip to dodge stale-base trap (net master→PR delta was -2184 LOC due to ~2-week-old base). Gates passed: - Pre-Opus: Python ast.parse on api/config.py, api/profiles.py, new test file - Stale-base check: cherry-picked diff matches contributor's actual change (6 files, +55/-17) - Opus advisor: SHIP-AS-IS, Linux no-op invariant verified - Full pytest sequential: 6540 passed, 6 skipped, 3 xpassed, 0 failures (179s) Closes #2840. --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac2e9d32..42919130 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ ## [Unreleased] +## [v0.51.134] — 2026-05-25 — Release DF (stage-batch16 — single-PR Windows path defaults) + ### Fixed - **PR #2897** by @chouzz — On Windows, WebUI default state and config paths now align with Hermes Agent's `%LOCALAPPDATA%\hermes` convention instead of `%USERPROFILE%\.hermes`, so a fresh Windows install finds the same `~/.hermes/config.yaml` / `auth.json` / `webui/` state directory that the Hermes Agent created. POSIX behavior is unchanged (`~/.hermes` remains the default). `HERMES_HOME` and `HERMES_WEBUI_STATE_DIR` env overrides take precedence on both platforms. Closes #2840.