# NousResearch/hermes-agent · Warn or detect running OpenClaw before claw migrate

## Open now

- 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.

## TL;DR

- 3 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 1 PR(s) closed without merging.
- A fix was merged to main in PR #8663. Verify it addresses your case.
- **Recommended:** read PR #8663 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `resolved` · deciding PR(s): `pr-8663`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.67`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #7907: hermes claw migrate should warn if OpenClaw is still running

## Prior & competing PRs

- **#8663: fix(claw): detect OpenClaw processes before migrate and cleanup (fixes #7907, #8502)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `hermes_cli/claw.py`, `tests/hermes_cli/test_claw.py`
  - fix hunks:
  - `hermes_cli/claw.py`

    ```diff
    @@ -11,6 +11,7 @@
     
     import importlib.util
     import logging
    +import subprocess
     import sys
     from datetime import datetime
     from pathlib import Path
    ```

    ```diff
    @@ -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.
     
    ```

    ```diff
    @@ -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
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -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
     
    ```

    ```diff
    @@ -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"),
    ```

    ```diff
    @@ -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
    ```
- **#8102: fix(claw): warn if OpenClaw is running before migration**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#8663`, `#8555`

## Fix status

- Merged to main in PR #8663 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-04-12`. This confirmation is old and may be out of date (a revert/reopen would not have invalidated it here); re-verify.
