Warn or detect running OpenClaw before claw migrate
Cluster 7907. Data as of
Nothing open on GitHub. Fix merged in #8663 on 12 Apr 2026. Check it covers your case.
The short answer
- Known?
- Yes. 3 related items grouped here, 1 issue already closed.
- Fix event
- A fix was merged to main in PR #8663. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
resolved, deciding PR #8663. That is separate from the fix event. - Still open
- Nothing open on GitHub.
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.
Open issues0
Open PRs0
Fixes on record
-
#8663 fix(claw): detect OpenClaw processes before migrate and cleanup (fixes #7907, #8502)
files:
hermes_cli/claw.py,tests/hermes_cli/test_claw.pyShow the diff
-
hermes_cli/claw.py@@ -11,6 +11,7 @@ import importlib.util import logging +import subprocess import sys from datetime import datetime from pathlib import Path @@ -52,6 +53,99 @@ # Known OpenClaw directory names (current + legacy) _OPENCLAW_DIR_NAMES = (".openclaw", ".clawdbot", ".moltbot") +def _detect_openclaw_processes() -> list[str]: + """Detect running OpenClaw processes and services. + + Returns a list of human-readable descriptions of what was found. + An empty list means nothing was detected. + """ + found: list[str] = [] + + # -- systemd service (Linux) ------------------------------------------ + if sys.platform != "win32": + try: + result = subprocess.run( + ["systemctl", "--user", "is-active", "openclaw-gateway.service"], + capture_output=True, text=True, timeout=5, + ) + if result.stdout.strip() == "active": + found.append("systemd service: openclaw-gateway.service") + except (FileNotFoundError, subprocess.TimeoutExpired): + pass + + # -- process scan ------------------------------------------------------ + if sys.platform == "win32": + try: + for exe in ("openclaw.exe", "clawd.exe"): + result = subprocess.run( + ["tasklist", "/FI", f"IMAGENAME eq {exe}"], + capture_output=True, text=True, timeout=5, + ) + if exe in result.stdout.lower(): + found.append(f"process: {exe}") + + # Node.js-hosted OpenClaw — tasklist doesn't show command lines, + # so fall back to PowerShell. + ps_cmd = ( + 'Get-CimInstance Win32_Process -Filter "Name = \'node.exe\'" | ' + 'Where-Object { $_.CommandLine -match "openclaw|clawd" } | ' + 'Select-Object -First 1 ProcessId' + ) + result = subprocess.run( + ["powershell", "-NoProfile", "-Command", ps_cmd], + capture_output=True, text=True, timeout=5, + ) + if result.stdout.strip(): + found.append(f"node.exe process with openclaw in command line (PID {result.stdout.strip()})") + except Exception: + pass + else: + try: + result = subprocess.run( + ["pgrep", "-f", "openclaw"], + capture_output=True, text=True, timeout=3, + ) + if result.returncode == 0: + pids = result.stdout.strip().split() + found.append(f"openclaw process(es) (PIDs: {', '.join(pids)})") + except (FileNotFoundError, subprocess.TimeoutExpired): + pass + + return found + + +def _warn_if_openclaw_running(auto_yes: bool) -> None: + """Warn if OpenClaw is still running before migration. + + Telegram, Discord, and Slack only allow one active connection per bot + token. Migrating while OpenClaw is running causes both to fight for the + same token. + """ + running = _detect_openclaw_processes() + if not running: + return + + print() + print_error("OpenClaw appears to be running:") + for detail in running: + print_info(f" * {detail}") + print_info( + "Messaging platforms (Telegram, Discord, Slack) only allow one " + "active session per bot token. If you continue, both OpenClaw and " + "Hermes may try to use the same token, causing disconnects." + ) + print_info("Recommendation: stop OpenClaw before migrating.") + print() + if auto_yes: + return + if not sys.stdin.isatty(): + print_info("Non-interactive session — continuing to preview only.") + return + if not prompt_yes_no("Continue anyway?", default=False): + print_info("Migration cancelled. Stop OpenClaw and try again.") + sys.exit(0) + + def _warn_if_gateway_running(auto_yes: bool) -> None: """Check if a Hermes gateway is running with connected platforms. @@ -287,8 +381,11 @@ def _cmd_migrate(args): print_info(f"Workspace: {workspace_target}") print() - # Check if a gateway is running with connected platforms — migrating tokens - # while the gateway is active will cause conflicts (e.g. Telegram 409). + # Check if OpenClaw is still running — migrating tokens while both are + # active will cause conflicts (e.g. Telegram 409). + _warn_if_openclaw_running(auto_yes) + + # Check if a Hermes gateway is running with connected platforms. _warn_if_gateway_running(auto_yes) # Ensure config.yaml exists before migration tries to read it @@ -430,6 +527,28 @@ def _cmd_cleanup(args): print_success("No OpenClaw directories found. Nothing to clean up.") return + # Warn if OpenClaw is still running — archiving while the service is + # active causes it to recreate an empty skeleton directory (#8502). + running = _detect_openclaw_processes() + if running: + print() + print_error("OpenClaw appears to be still running:") + for detail in running: + print_info(f" * {detail}") + print_info( + "Archiving .openclaw/ while the service is active may cause it to " + "immediately recreate an empty skeleton directory, destroying your config." + ) + print_info("Stop OpenClaw first: systemctl --user stop openclaw-gateway.service") + print() + if not auto_yes: + if not sys.stdin.isatty(): + print_info("Non-interactive session — aborting. Stop OpenClaw and re-run.") + return + if not prompt_yes_no("Proceed anyway?", default=False): + print_info("Aborted. Stop OpenClaw first, then re-run: hermes claw cleanup") + return + total_archived = 0 for source_dir in dirs_to_check: -
tests/hermes_cli/test_claw.py@@ -1,6 +1,7 @@ """Tests for hermes claw commands.""" from argparse import Namespace +import subprocess from types import ModuleType from unittest.mock import MagicMock, patch @@ -197,6 +198,11 @@ def test_shows_help_for_no_action(self, capsys): class TestCmdMigrate: """Test the migrate command handler.""" + @pytest.fixture(autouse=True) + def _mock_openclaw_running(self): + with patch.object(claw_mod, "_detect_openclaw_processes", return_value=[]): + yield + def test_error_when_source_missing(self, tmp_path, capsys): args = Namespace( source=str(tmp_path / "nonexistent"), @@ -626,3 +632,120 @@ def test_empty_report(self, capsys): claw_mod._print_migration_report(report, dry_run=False) captured = capsys.readouterr() assert "Nothing to migrate" in captured.out + + +class TestDetectOpenclawProcesses: + def test_returns_match_when_pgrep_finds_openclaw(self): + with patch.object(claw_mod, "sys") as mock_sys: + mock_sys.platform = "linux" + with patch.object(claw_mod, "subprocess") as mock_subprocess: + # systemd check misses, pgrep finds openclaw + mock_subprocess.run.side_effect = [ + MagicMock(returncode=1, stdout=""), # systemctl + MagicMock(returncode=0, stdout="1234\n"), # pgrep + ] + mock_subprocess.TimeoutExpired = subprocess.TimeoutExpired + result = claw_mod._detect_openclaw_processes() + assert len(result) == 1 + assert "1234" in result[0] + + def test_returns_empty_when_pgrep_finds_nothing(self): + with patch.object(claw_mod, "sys") as mock_sys: + mock_sys.platform = "darwin" + with patch.object(claw_mod, "subprocess") as mock_subprocess: + mock_subprocess.run.side_effect = [ + MagicMock(returncode=1, stdout=""), # systemctl (not found) + MagicMock(returncode=1, stdout=""), # pgrep + ] + mock_subprocess.TimeoutExpired = subprocess.TimeoutExpired + result = claw_mod._detect_openclaw_processes() + assert result == [] + + def test_detects_systemd_service(self): + with patch.object(claw_mod, "sys") as mock_sys: + mock_sys.platform = "linux" + with patch.object(claw_mod, "subprocess") as mock_subprocess: + mock_subprocess.run.side_effect = [ + MagicMock(returncode=0, stdout="active\n"), # systemctl + MagicMock(returncode=1, stdout=""), # pgrep + ] + mock_subprocess.TimeoutExpired = subprocess.TimeoutExpired + result = claw_mod._detect_openclaw_processes() + assert len(result) == 1 + assert "systemd" in result[0] + + def test_returns_match_on_windows_when_openclaw_exe_running(self): + with patch.object(claw_mod, "sys") as mock_sys: + mock_sys.platform = "win32" + with patch.object(claw_mod, "subprocess") as mock_subprocess: + mock_subprocess.run.side_effect = [ + MagicMock(returncode=0, stdout="openclaw.exe 1234 Console 1 45,056 K\n"), + ] + result = claw_mod._detect_openclaw_processes() + assert len(result) >= 1 + assert any("openclaw.exe" in r for r in result) + + def test_returns_match_on_windows_when_node_exe_has_openclaw_in_cmdline(self): + with patch.object(claw_mod, "sys") as mock_sys: + mock_sys.platform = "win32" + with patch.object(claw_mod, "subprocess") as mock_subprocess: + mock_subprocess.run.side_effect = [ + MagicMock(returncode=0, stdout=""), # tasklist openclaw.exe + MagicMock(returncode=0, stdout=""), # tasklist clawd.exe + MagicMock(returncode=0, stdout="1234\n"), # PowerShell + ] + result = claw_mod._detect_openclaw_processes() + assert len(result) >= 1 + assert any("node.exe" in r for r in result) + + def test_returns_empty_on_windows_when_nothing_found(self): + with patch.object(claw_mod, "sys") as mock_sys: + mock_sys.platform = "win32" + with patch.object(claw_mod, "subprocess") as mock_subprocess: + mock_subprocess.run.side_effect = [ + MagicMock(returncode=0, stdout=""), + MagicMock(returncode=0, stdout=""), + MagicMock(returncode=0, stdout=""), + ] + result = claw_mod._detect_openclaw_processes() + assert result == [] + + +class TestWarnIfOpenclawRunning: + def test_noop_when_not_running(self, capsys): + with patch.object(claw_mod, "_detect_openclaw_processes", return_value=[]): + claw_mod._warn_if_openclaw_running(auto_yes=False) + captured = capsys.readouterr() + assert captured.out == "" + + def test_warns_and_exits_when_running_and_user_declines(self, capsys): + with patch.object(claw_mod, "_detect_openclaw_processes", return_value=["openclaw process(es) (PIDs: 1234)"]): + with patch.object(claw_mod, "prompt_yes_no", return_value=False): + with patch.object(claw_mod.sys.stdin, "isatty", return_value=True): + with pytest.raises(SystemExit) as exc_info: + claw_mod._warn_if_openclaw_running(auto_yes=False) + assert exc_info.value.code == 0 + captured = capsys.readouterr() + assert "OpenClaw appears to be running" in captured.out + + def test_warns_and_continues_when_running_and_user_accepts(self, capsys): + with patch.object(claw_mod, "_detect_openclaw_processes", return_value=["openclaw process(es) (PIDs: 1234)"]): + with patch.object(claw_mod, "prompt_yes_no", return_value=True): + with patch.object(claw_mod.sys.stdin, "isatty", return_value=True): + claw_mod._warn_if_openclaw_running(auto_yes=False) + captured = capsys.readouterr() + assert "OpenClaw appears to be running" in captured.out + + def test_warns_and_continues_in_auto_yes_mode(self, capsys): + with patch.object(claw_mod, "_detect_openclaw_processes", return_value=["openclaw process(es) (PIDs: 1234)"]): + claw_mod._warn_if_openclaw_running(auto_yes=True) + captured = capsys.readouterr() + assert "OpenClaw appears to be running" in captured.out + + def test_warns_and_continues_in_non_interactive_session(self, capsys): + with patch.object(claw_mod, "_detect_openclaw_processes", return_value=["openclaw process(es) (PIDs: 1234)"]): + with patch.object(claw_mod.sys.stdin, "isatty", return_value=False): + claw_mod._warn_if_openclaw_running(auto_yes=False) + captured = capsys.readouterr() + assert "OpenClaw appears to be running" in captured.out + assert "Non-interactive session" in captured.out
-
1 PR closed without merging
From the close comments: merged via another change 1
-
#8102 fix(claw): warn if OpenClaw is running before migration · merged via another change →
#8663,#8555
Closed issues · 1
- Closed#7907hermes claw migrate should warn if OpenClaw is still running
How this cluster was grouped
-
Link evidence (
structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference. -
Pairwise link density
0.67: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the LabAt a glance
- Merged PRs
- #8663
- Closed issues
- 1
- Attempts
- 1 PR closed without merging
- Fix link checked
- 12 Apr 2026. This confirmation is old and may be out of date (a revert or reopen would not have invalidated it here); re-verify.