{"repo":"NousResearch/hermes-agent","public_id":"7907","display_label":"Warn or detect running OpenClaw before claw migrate","summary":"These items share one theme: checking whether OpenClaw processes are running before `hermes claw migrate` (and, in one title, cleanup) and warning the user. One is an issue requesting the warning, and two are pull requests proposing detection or warning behavior; their outcomes are not stated in the titles.","subsystem":null,"counts":{"members":3,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":8663,"merged_at":"2026-04-12T23:40:38+00:00"},"resolution":{"state":"resolved","by":"pr-8663"},"freshness":{"state":"stale","last_verified_at":"2026-04-12T23:40:38+00:00","is_stale":true},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":7907,"pr_number":8663,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-04-12T23:40:38+00:00"},{"issue_number":8502,"pr_number":8663,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-04-12T23:40:38+00:00"}],"members":[{"kind":"issue","number":7907,"title":"hermes claw migrate should warn if OpenClaw is still running","url":null,"state":"closed"},{"kind":"pr","number":8102,"title":"fix(claw): warn if OpenClaw is running before migration","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#8663","number":8663},{"kind":"pr","value":"#8555","number":8555}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":8663,"title":"fix(claw): detect OpenClaw processes before migrate and cleanup (fixes #7907, #8502)","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["hermes_cli/claw.py","tests/hermes_cli/test_claw.py"],"fix_hunks":[{"file":"hermes_cli/claw.py","hunks":["@@ -11,6 +11,7 @@\n \n import importlib.util\n import logging\n+import subprocess\n import sys\n from datetime import datetime\n from pathlib import Path","@@ -52,6 +53,99 @@\n # Known OpenClaw directory names (current + legacy)\n _OPENCLAW_DIR_NAMES = (\".openclaw\", \".clawdbot\", \".moltbot\")\n \n+def _detect_openclaw_processes() -> list[str]:\n+    \"\"\"Detect running OpenClaw processes and services.\n+\n+    Returns a list of human-readable descriptions of what was found.\n+    An empty list means nothing was detected.\n+    \"\"\"\n+    found: list[str] = []\n+\n+    # -- systemd service (Linux) ------------------------------------------\n+    if sys.platform != \"win32\":\n+        try:\n+            result = subprocess.run(\n+                [\"systemctl\", \"--user\", \"is-active\", \"openclaw-gateway.service\"],\n+                capture_output=True, text=True, timeout=5,\n+            )\n+            if result.stdout.strip() == \"active\":\n+                found.append(\"systemd service: openclaw-gateway.service\")\n+        except (FileNotFoundError, subprocess.TimeoutExpired):\n+            pass\n+\n+    # -- process scan ------------------------------------------------------\n+    if sys.platform == \"win32\":\n+        try:\n+            for exe in (\"openclaw.exe\", \"clawd.exe\"):\n+                result = subprocess.run(\n+                    [\"tasklist\", \"/FI\", f\"IMAGENAME eq {exe}\"],\n+                    capture_output=True, text=True, timeout=5,\n+                )\n+                if exe in result.stdout.lower():\n+                    found.append(f\"process: {exe}\")\n+\n+            # Node.js-hosted OpenClaw — tasklist doesn't show command lines,\n+            # so fall back to PowerShell.\n+            ps_cmd = (\n+                'Get-CimInstance Win32_Process -Filter \"Name = \\'node.exe\\'\" | '\n+                'Where-Object { $_.CommandLine -match \"openclaw|clawd\" } | '\n+                'Select-Object -First 1 ProcessId'\n+            )\n+            result = subprocess.run(\n+                [\"powershell\", \"-NoProfile\", \"-Command\", ps_cmd],\n+                capture_output=True, text=True, timeout=5,\n+            )\n+            if result.stdout.strip():\n+                found.append(f\"node.exe process with openclaw in command line (PID {result.stdout.strip()})\")\n+        except Exception:\n+            pass\n+    else:\n+        try:\n+            result = subprocess.run(\n+                [\"pgrep\", \"-f\", \"openclaw\"],\n+                capture_output=True, text=True, timeout=3,\n+            )\n+            if result.returncode == 0:\n+                pids = result.stdout.strip().split()\n+                found.append(f\"openclaw process(es) (PIDs: {', '.join(pids)})\")\n+        except (FileNotFoundError, subprocess.TimeoutExpired):\n+            pass\n+\n+    return found\n+\n+\n+def _warn_if_openclaw_running(auto_yes: bool) -> None:\n+    \"\"\"Warn if OpenClaw is still running before migration.\n+\n+    Telegram, Discord, and Slack only allow one active connection per bot\n+    token. Migrating while OpenClaw is running causes both to fight for the\n+    same token.\n+    \"\"\"\n+    running = _detect_openclaw_processes()\n+    if not running:\n+        return\n+\n+    print()\n+    print_error(\"OpenClaw appears to be running:\")\n+    for detail in running:\n+        print_info(f\"  * {detail}\")\n+    print_info(\n+        \"Messaging platforms (Telegram, Discord, Slack) only allow one \"\n+        \"active session per bot token. If you continue, both OpenClaw and \"\n+        \"Hermes may try to use the same token, causing disconnects.\"\n+    )\n+    print_info(\"Recommendation: stop OpenClaw before migrating.\")\n+    print()\n+    if auto_yes:\n+        return\n+    if not sys.stdin.isatty():\n+        print_info(\"Non-interactive session — continuing to preview only.\")\n+        return\n+    if not prompt_yes_no(\"Continue anyway?\", default=False):\n+        print_info(\"Migration cancelled. Stop OpenClaw and try again.\")\n+        sys.exit(0)\n+\n+\n def _warn_if_gateway_running(auto_yes: bool) -> None:\n     \"\"\"Check if a Hermes gateway is running with connected platforms.\n ","@@ -287,8 +381,11 @@ def _cmd_migrate(args):\n         print_info(f\"Workspace:   {workspace_target}\")\n     print()\n \n-    # Check if a gateway is running with connected platforms — migrating tokens\n-    # while the gateway is active will cause conflicts (e.g. Telegram 409).\n+    # Check if OpenClaw is still running — migrating tokens while both are\n+    # active will cause conflicts (e.g. Telegram 409).\n+    _warn_if_openclaw_running(auto_yes)\n+\n+    # Check if a Hermes gateway is running with connected platforms.\n     _warn_if_gateway_running(auto_yes)\n \n     # Ensure config.yaml exists before migration tries to read it","@@ -430,6 +527,28 @@ def _cmd_cleanup(args):\n         print_success(\"No OpenClaw directories found. Nothing to clean up.\")\n         return\n \n+    # Warn if OpenClaw is still running — archiving while the service is\n+    # active causes it to recreate an empty skeleton directory (#8502).\n+    running = _detect_openclaw_processes()\n+    if running:\n+        print()\n+        print_error(\"OpenClaw appears to be still running:\")\n+        for detail in running:\n+            print_info(f\"  * {detail}\")\n+        print_info(\n+            \"Archiving .openclaw/ while the service is active may cause it to \"\n+            \"immediately recreate an empty skeleton directory, destroying your config.\"\n+        )\n+        print_info(\"Stop OpenClaw first: systemctl --user stop openclaw-gateway.service\")\n+        print()\n+        if not auto_yes:\n+            if not sys.stdin.isatty():\n+                print_info(\"Non-interactive session — aborting. Stop OpenClaw and re-run.\")\n+                return\n+            if not prompt_yes_no(\"Proceed anyway?\", default=False):\n+                print_info(\"Aborted. Stop OpenClaw first, then re-run: hermes claw cleanup\")\n+                return\n+\n     total_archived = 0\n \n     for source_dir in dirs_to_check:"]},{"file":"tests/hermes_cli/test_claw.py","hunks":["@@ -1,6 +1,7 @@\n \"\"\"Tests for hermes claw commands.\"\"\"\n \n from argparse import Namespace\n+import subprocess\n from types import ModuleType\n from unittest.mock import MagicMock, patch\n ","@@ -197,6 +198,11 @@ def test_shows_help_for_no_action(self, capsys):\n class TestCmdMigrate:\n     \"\"\"Test the migrate command handler.\"\"\"\n \n+    @pytest.fixture(autouse=True)\n+    def _mock_openclaw_running(self):\n+        with patch.object(claw_mod, \"_detect_openclaw_processes\", return_value=[]):\n+            yield\n+\n     def test_error_when_source_missing(self, tmp_path, capsys):\n         args = Namespace(\n             source=str(tmp_path / \"nonexistent\"),","@@ -626,3 +632,120 @@ def test_empty_report(self, capsys):\n         claw_mod._print_migration_report(report, dry_run=False)\n         captured = capsys.readouterr()\n         assert \"Nothing to migrate\" in captured.out\n+\n+\n+class TestDetectOpenclawProcesses:\n+    def test_returns_match_when_pgrep_finds_openclaw(self):\n+        with patch.object(claw_mod, \"sys\") as mock_sys:\n+            mock_sys.platform = \"linux\"\n+            with patch.object(claw_mod, \"subprocess\") as mock_subprocess:\n+                # systemd check misses, pgrep finds openclaw\n+                mock_subprocess.run.side_effect = [\n+                    MagicMock(returncode=1, stdout=\"\"),  # systemctl\n+                    MagicMock(returncode=0, stdout=\"1234\\n\"),  # pgrep\n+                ]\n+                mock_subprocess.TimeoutExpired = subprocess.TimeoutExpired\n+                result = claw_mod._detect_openclaw_processes()\n+                assert len(result) == 1\n+                assert \"1234\" in result[0]\n+\n+    def test_returns_empty_when_pgrep_finds_nothing(self):\n+        with patch.object(claw_mod, \"sys\") as mock_sys:\n+            mock_sys.platform = \"darwin\"\n+            with patch.object(claw_mod, \"subprocess\") as mock_subprocess:\n+                mock_subprocess.run.side_effect = [\n+                    MagicMock(returncode=1, stdout=\"\"),  # systemctl (not found)\n+                    MagicMock(returncode=1, stdout=\"\"),  # pgrep\n+                ]\n+                mock_subprocess.TimeoutExpired = subprocess.TimeoutExpired\n+                result = claw_mod._detect_openclaw_processes()\n+                assert result == []\n+\n+    def test_detects_systemd_service(self):\n+        with patch.object(claw_mod, \"sys\") as mock_sys:\n+            mock_sys.platform = \"linux\"\n+            with patch.object(claw_mod, \"subprocess\") as mock_subprocess:\n+                mock_subprocess.run.side_effect = [\n+                    MagicMock(returncode=0, stdout=\"active\\n\"),  # systemctl\n+                    MagicMock(returncode=1, stdout=\"\"),  # pgrep\n+                ]\n+                mock_subprocess.TimeoutExpired = subprocess.TimeoutExpired\n+                result = claw_mod._detect_openclaw_processes()\n+                assert len(result) == 1\n+                assert \"systemd\" in result[0]\n+\n+    def test_returns_match_on_windows_when_openclaw_exe_running(self):\n+        with patch.object(claw_mod, \"sys\") as mock_sys:\n+            mock_sys.platform = \"win32\"\n+            with patch.object(claw_mod, \"subprocess\") as mock_subprocess:\n+                mock_subprocess.run.side_effect = [\n+                    MagicMock(returncode=0, stdout=\"openclaw.exe                 1234 Console    1     45,056 K\\n\"),\n+                ]\n+                result = claw_mod._detect_openclaw_processes()\n+                assert len(result) >= 1\n+                assert any(\"openclaw.exe\" in r for r in result)\n+\n+    def test_returns_match_on_windows_when_node_exe_has_openclaw_in_cmdline(self):\n+        with patch.object(claw_mod, \"sys\") as mock_sys:\n+            mock_sys.platform = \"win32\"\n+            with patch.object(claw_mod, \"subprocess\") as mock_subprocess:\n+                mock_subprocess.run.side_effect = [\n+                    MagicMock(returncode=0, stdout=\"\"),  # tasklist openclaw.exe\n+                    MagicMock(returncode=0, stdout=\"\"),  # tasklist clawd.exe\n+                    MagicMock(returncode=0, stdout=\"1234\\n\"),  # PowerShell\n+                ]\n+                result = claw_mod._detect_openclaw_processes()\n+                assert len(result) >= 1\n+                assert any(\"node.exe\" in r for r in result)\n+\n+    def test_returns_empty_on_windows_when_nothing_found(self):\n+        with patch.object(claw_mod, \"sys\") as mock_sys:\n+            mock_sys.platform = \"win32\"\n+            with patch.object(claw_mod, \"subprocess\") as mock_subprocess:\n+                mock_subprocess.run.side_effect = [\n+                    MagicMock(returncode=0, stdout=\"\"),\n+                    MagicMock(returncode=0, stdout=\"\"),\n+                    MagicMock(returncode=0, stdout=\"\"),\n+                ]\n+                result = claw_mod._detect_openclaw_processes()\n+                assert result == []\n+\n+\n+class TestWarnIfOpenclawRunning:\n+    def test_noop_when_not_running(self, capsys):\n+        with patch.object(claw_mod, \"_detect_openclaw_processes\", return_value=[]):\n+            claw_mod._warn_if_openclaw_running(auto_yes=False)\n+        captured = capsys.readouterr()\n+        assert captured.out == \"\"\n+\n+    def test_warns_and_exits_when_running_and_user_declines(self, capsys):\n+        with patch.object(claw_mod, \"_detect_openclaw_processes\", return_value=[\"openclaw process(es) (PIDs: 1234)\"]):\n+            with patch.object(claw_mod, \"prompt_yes_no\", return_value=False):\n+                with patch.object(claw_mod.sys.stdin, \"isatty\", return_value=True):\n+                    with pytest.raises(SystemExit) as exc_info:\n+                        claw_mod._warn_if_openclaw_running(auto_yes=False)\n+        assert exc_info.value.code == 0\n+        captured = capsys.readouterr()\n+        assert \"OpenClaw appears to be running\" in captured.out\n+\n+    def test_warns_and_continues_when_running_and_user_accepts(self, capsys):\n+        with patch.object(claw_mod, \"_detect_openclaw_processes\", return_value=[\"openclaw process(es) (PIDs: 1234)\"]):\n+            with patch.object(claw_mod, \"prompt_yes_no\", return_value=True):\n+                with patch.object(claw_mod.sys.stdin, \"isatty\", return_value=True):\n+                    claw_mod._warn_if_openclaw_running(auto_yes=False)\n+        captured = capsys.readouterr()\n+        assert \"OpenClaw appears to be running\" in captured.out\n+\n+    def test_warns_and_continues_in_auto_yes_mode(self, capsys):\n+        with patch.object(claw_mod, \"_detect_openclaw_processes\", return_value=[\"openclaw process(es) (PIDs: 1234)\"]):\n+            claw_mod._warn_if_openclaw_running(auto_yes=True)\n+        captured = capsys.readouterr()\n+        assert \"OpenClaw appears to be running\" in captured.out\n+\n+    def test_warns_and_continues_in_non_interactive_session(self, capsys):\n+        with patch.object(claw_mod, \"_detect_openclaw_processes\", return_value=[\"openclaw process(es) (PIDs: 1234)\"]):\n+            with patch.object(claw_mod.sys.stdin, \"isatty\", return_value=False):\n+                claw_mod._warn_if_openclaw_running(auto_yes=False)\n+        captured = capsys.readouterr()\n+        assert \"OpenClaw appears to be running\" in captured.out\n+        assert \"Non-interactive session\" in captured.out\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}