{"repo":"NousResearch/hermes-agent","public_id":"6989","display_label":"System service path mismatch when installed as root","summary":"A bug where the gateway system service unit uses incorrect paths when installed as root, and related fixes to remap Python, venv, and working directory paths to the target user's home directory.","subsystem":"hermes_cli","counts":{"members":6,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":2,"merged_prs":1,"attempt_prs":3},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":7109},"resolution":{"state":"partially_resolved","by":"pr-7109"},"freshness":{"state":"stale","last_verified_at":"2026-04-10T10:30:37+00:00","is_stale":true},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.29066666666666663},"fix_edges":[{"issue_number":6989,"pr_number":7109,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true}],"members":[{"kind":"issue","number":6989,"title":"[Bug] System gateway service fails with CHDIR error when installed as root — path mismatch between User= and WorkingDirectory/ExecStart","url":null,"state":"closed"},{"kind":"issue","number":25282,"title":"hermes update resets gateway systemd unit paths to CLI user home, breaking service account setups","url":null,"state":"closed"},{"kind":"pr","number":7016,"title":"fix(gateway): remap python/venv/workdir to target user when installing system service as root","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#7109","number":7109}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":7030,"title":"fix(gateway): remap all paths in system service unit to target user's home","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#7109","number":7109}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":7109,"title":"fix(gateway): remap all paths in system service unit to target user's home","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["hermes_cli/gateway.py","tests/hermes_cli/test_gateway_service.py"],"fix_hunks":[{"file":"hermes_cli/gateway.py","hunks":["@@ -618,6 +618,24 @@ def _build_user_local_paths(home: Path, path_entries: list[str]) -> list[str]:\n     return [p for p in candidates if p not in path_entries and Path(p).exists()]\n \n \n+def _remap_path_for_user(path: str, target_home_dir: str) -> str:\n+    \"\"\"Remap *path* from the current user's home to *target_home_dir*.\n+\n+    If *path* lives under ``Path.home()`` the corresponding prefix is swapped\n+    to *target_home_dir*; otherwise the path is returned unchanged.\n+\n+      /root/.hermes/hermes-agent  -> /home/alice/.hermes/hermes-agent\n+      /opt/hermes                 -> /opt/hermes  (kept as-is)\n+    \"\"\"\n+    current_home = Path.home().resolve()\n+    resolved = Path(path).resolve()\n+    try:\n+        relative = resolved.relative_to(current_home)\n+        return str(Path(target_home_dir) / relative)\n+    except ValueError:\n+        return str(resolved)\n+\n+\n def _hermes_home_for_target_user(target_home_dir: str) -> str:\n     \"\"\"Remap the current HERMES_HOME to the equivalent under a target user's home.\n ","@@ -665,6 +683,15 @@ def generate_systemd_unit(system: bool = False, run_as_user: str | None = None)\n         username, group_name, home_dir = _system_service_identity(run_as_user)\n         hermes_home = _hermes_home_for_target_user(home_dir)\n         profile_arg = _profile_arg(hermes_home)\n+        # Remap all paths that may resolve under the calling user's home\n+        # (e.g. /root/) to the target user's home so the service can\n+        # actually access them.\n+        python_path = _remap_path_for_user(python_path, home_dir)\n+        working_dir = _remap_path_for_user(working_dir, home_dir)\n+        venv_dir = _remap_path_for_user(venv_dir, home_dir)\n+        venv_bin = _remap_path_for_user(venv_bin, home_dir)\n+        node_bin = _remap_path_for_user(node_bin, home_dir)\n+        path_entries = [_remap_path_for_user(p, home_dir) for p in path_entries]\n         path_entries.extend(_build_user_local_paths(Path(home_dir), path_entries))\n         path_entries.extend(common_bin_paths)\n         sane_path = \":\".join(path_entries)"]},{"file":"tests/hermes_cli/test_gateway_service.py","hunks":["@@ -754,3 +754,63 @@ def test_launchd_plist_includes_profile(self, tmp_path, monkeypatch):\n         plist = gateway_cli.generate_launchd_plist()\n         assert \"<string>--profile</string>\" in plist\n         assert \"<string>mybot</string>\" in plist\n+\n+\n+class TestRemapPathForUser:\n+    \"\"\"Unit tests for _remap_path_for_user().\"\"\"\n+\n+    def test_remaps_path_under_current_home(self, monkeypatch, tmp_path):\n+        monkeypatch.setattr(Path, \"home\", lambda: tmp_path / \"root\")\n+        (tmp_path / \"root\").mkdir()\n+        result = gateway_cli._remap_path_for_user(\n+            str(tmp_path / \"root\" / \".hermes\" / \"hermes-agent\"),\n+            str(tmp_path / \"alice\"),\n+        )\n+        assert result == str(tmp_path / \"alice\" / \".hermes\" / \"hermes-agent\")\n+\n+    def test_keeps_system_path_unchanged(self, monkeypatch, tmp_path):\n+        monkeypatch.setattr(Path, \"home\", lambda: tmp_path / \"root\")\n+        (tmp_path / \"root\").mkdir()\n+        result = gateway_cli._remap_path_for_user(\"/opt/hermes\", str(tmp_path / \"alice\"))\n+        assert result == \"/opt/hermes\"\n+\n+    def test_noop_when_same_user(self, monkeypatch, tmp_path):\n+        monkeypatch.setattr(Path, \"home\", lambda: tmp_path / \"alice\")\n+        (tmp_path / \"alice\").mkdir()\n+        original = str(tmp_path / \"alice\" / \".hermes\" / \"hermes-agent\")\n+        result = gateway_cli._remap_path_for_user(original, str(tmp_path / \"alice\"))\n+        assert result == original\n+\n+\n+class TestSystemUnitPathRemapping:\n+    \"\"\"System units must remap ALL paths from the caller's home to the target user.\"\"\"\n+\n+    def test_system_unit_has_no_root_paths(self, monkeypatch, tmp_path):\n+        root_home = tmp_path / \"root\"\n+        root_home.mkdir()\n+        project = root_home / \".hermes\" / \"hermes-agent\"\n+        project.mkdir(parents=True)\n+        venv_bin = project / \"venv\" / \"bin\"\n+        venv_bin.mkdir(parents=True)\n+        (venv_bin / \"python\").write_text(\"\")\n+\n+        target_home = \"/home/alice\"\n+\n+        monkeypatch.setattr(Path, \"home\", lambda: root_home)\n+        monkeypatch.setenv(\"HERMES_HOME\", str(root_home / \".hermes\"))\n+        monkeypatch.setattr(gateway_cli, \"get_hermes_home\", lambda: root_home / \".hermes\")\n+        monkeypatch.setattr(gateway_cli, \"PROJECT_ROOT\", project)\n+        monkeypatch.setattr(gateway_cli, \"_detect_venv_dir\", lambda: project / \"venv\")\n+        monkeypatch.setattr(gateway_cli, \"get_python_path\", lambda: str(venv_bin / \"python\"))\n+        monkeypatch.setattr(\n+            gateway_cli, \"_system_service_identity\",\n+            lambda run_as_user=None: (\"alice\", \"alice\", target_home),\n+        )\n+\n+        unit = gateway_cli.generate_systemd_unit(system=True)\n+\n+        # No root paths should leak into the unit\n+        assert str(root_home) not in unit\n+        # Target user paths should be present\n+        assert \"/home/alice\" in unit\n+        assert \"WorkingDirectory=/home/alice/.hermes/hermes-agent\" in unit\n"]}]},{"kind":"pr","number":25298,"title":"fix(cli): preserve systemd unit paths when service user differs from CLI user","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"a1cb5fa2c7cd5239a4909261888453d97086986d","number":0}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}