NousResearch/hermes-agent

OpenClaw migration: source directory archiving and Hermes rebranding

Cluster 8188. Data as of

Nothing open on GitHub. Fix merged in #8210 on 12 Apr 2026. Check it covers your case.

The short answer

0open issues
0open PRs
3items grouped
Known?
Yes. 3 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #8210. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this resolved, deciding PR #8210. That is separate from the fix event.
Still open
Nothing open on GitHub.

Two of the three members concern the migration process not auto-archiving the OpenClaw source directory, with one also asking for coexistence guidance. The third proposes rebranding OpenClaw references to Hermes during migration, which is related to migration but a separate change.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #8210 feat: rebrand OpenClaw references to Hermes during migration

    merged to main

    files: docs/migration/openclaw.md, hermes_cli/claw.py, optional-skills/migration/openclaw-migration/scripts/openclaw_to_hermes.py, tests/hermes_cli/test_claw.py, tests/skills/test_openclaw_migration.py, website/docs/guides/migrate-from-openclaw.md, website/docs/reference/cli-commands.md

    Show the diff
    • docs/migration/openclaw.md
      @@ -118,7 +118,7 @@ For executed migrations, the full report is saved to `~/.hermes/migration/opencl
       ## Troubleshooting
       
       ### "OpenClaw directory not found"
      -The migration looks for `~/.openclaw` by default, then tries `~/.clawdbot` and `~/.moldbot`. If your OpenClaw is installed elsewhere, use `--source`:
      +The migration looks for `~/.openclaw` by default, then tries `~/.clawdbot` and `~/.moltbot`. If your OpenClaw is installed elsewhere, use `--source`:
       ```bash
       hermes claw migrate --source /path/to/.openclaw
       ```
    • hermes_cli/claw.py
      @@ -50,7 +50,7 @@
       )
       
       # Known OpenClaw directory names (current + legacy)
      -_OPENCLAW_DIR_NAMES = (".openclaw", ".clawdbot", ".moldbot")
      +_OPENCLAW_DIR_NAMES = (".openclaw", ".clawdbot", ".moltbot")
       
       def _warn_if_gateway_running(auto_yes: bool) -> None:
           """Check if a Hermes gateway is running with connected platforms.
      @@ -87,8 +87,8 @@ def _warn_if_gateway_running(auto_yes: bool) -> None:
               print_info("Migration cancelled. Stop the gateway and try again.")
               sys.exit(0)
       
      -# State files commonly found in OpenClaw workspace directories that cause
      -# confusion after migration (the agent discovers them and writes to them)
      +# State files commonly found in OpenClaw workspace directories — listed
      +# during cleanup to help the user decide whether to archive
       _WORKSPACE_STATE_GLOBS = (
           "*/todo.json",
           "*/sessions/*",
      @@ -133,7 +133,7 @@ def _find_openclaw_dirs() -> list[Path]:
       
       
       def _scan_workspace_state(source_dir: Path) -> list[tuple[Path, str]]:
      -    """Scan an OpenClaw directory for workspace state files that cause confusion.
      +    """Scan an OpenClaw directory for workspace state files.
       
           Returns a list of (path, description) tuples.
           """
      @@ -216,7 +216,7 @@ def _cmd_migrate(args):
               source_dir = Path.home() / ".openclaw"
               if not source_dir.is_dir():
                   # Try legacy directory names
      -            for legacy in (".clawdbot", ".moldbot"):
      +            for legacy in (".clawdbot", ".moltbot"):
                       candidate = Path.home() / legacy
                       if candidate.is_dir():
                           source_dir = candidate
      @@ -384,65 +384,16 @@ def _cmd_migrate(args):
           # Print results
           _print_migration_report(report, dry_run=False)
       
      -    # After successful migration, offer to archive the source directory
      -    if report.get("summary", {}).get("migrated", 0) > 0:
      -        _offer_source_archival(source_dir, auto_yes)
      -
      -
      -def _offer_source_archival(source_dir: Path, auto_yes: bool = False):
      -    """After migration, offer to rename the source directory to prevent state fragmentation.
      -
      -    OpenClaw workspace directories contain state files (todo.json, sessions, etc.)
      -    that the agent may discover and write to, causing confusion. Renaming the
      -    directory prevents this.
      -    """
      -    if not source_dir.is_dir():
      -        return
      -
      -    # Scan for state files that could cause problems
      -    state_files = _scan_workspace_state(source_dir)
      -
      -    print()
      -    print_header("Post-Migration Cleanup")
      -    print_info("The OpenClaw directory still exists and contains workspace state files")
      -    print_info("that can confuse the agent (todo lists, sessions, logs).")
      -    if state_files:
      -        print()
      -        print(color("  Found state files:", Colors.YELLOW))
      -        # Show up to 10 most relevant findings
      -        for path, desc in state_files[:10]:
      -            print(f"      {desc}")
      -        if len(state_files) > 10:
      -            print(f"      ... and {len(state_files) - 10} more")
      -    print()
      -    print_info(f"Recommend: rename {source_dir.name}/ to {source_dir.name}.pre-migration/")
      -    print_info("This prevents the agent from discovering old workspace directories.")
      -    print_info("You can always rename it back if needed.")
      -    print()
      -
      -    if not auto_yes and not sys.stdin.isatty():
      -        print_info("Non-interactive session — skipping archival.")
      -        print_info("Run later with: hermes claw cleanup")
      -        return
      -
      -    if auto_yes or prompt_yes_no(f"Archive {source_dir} now?", default=True):
      -        try:
      -            archive_path = _archive_directory(source_dir)
      -            print_success(f"Archived: {source_dir} → {archive_path}")
      -            print_info("The original directory has been renamed, not deleted.")
      -            print_info(f"To undo: mv {archive_path} {source_dir}")
      -        except OSError as e:
      -            print_error(f"Could not archive: {e}")
      -            print_info(f"You can do it manually: mv {source_dir} {source_dir}.pre-migration")
      -    else:
      -        print_info("Skipped. You can archive later with: hermes claw cleanup")
      +    # Source directory is left untouched — archiving is not the migration
      +    # tool's responsibility.  Users who want to clean up can run
      +    # 'hermes claw cleanup' separately.
       
       
       def _cmd_cleanup(args):
           """Archive leftover OpenClaw directories after migration.
       
           Scans for OpenClaw directories that still exist after migration and offers
      -    to rename them to .pre-migration to prevent state fragmentation.
      +    to rename them to .pre-migration to free disk space.
           """
           dry_run = getattr(args, "dry_run", False)
           auto_yes = getattr(args, "yes", False)
      @@ -517,7 +468,7 @@ def _cmd_cleanup(args):
       
               if state_files:
                   print()
      -            print(color(f"  {len(state_files)} state file(s) that could cause confusion:", Colors.YELLOW))
      +            print(color(f"  {len(state_files)} state file(s) found:", Colors.YELLOW))
                   for path, desc in state_files[:8]:
                       print(f"      {desc}")
                   if len(state_files) > 8:
    • optional-skills/migration/openclaw-migration/scripts/openclaw_to_hermes.py
      @@ -376,6 +376,24 @@ def backup_existing(path: Path, backup_root: Path) -> Optional[Path]:
           return dest
       
       
      +# ── Brand rewriting ─────────────────────────────────────────
      +# Replace OpenClaw brand names with Hermes in migrated text so that
      +# memory entries, user profiles, SOUL.md, and workspace instructions
      +# read as self-referential to the new agent identity.
      +_REBRAND_PATTERNS: List[Tuple[re.Pattern, str]] = [
      +    (re.compile(r'\bOpen[\s-]?Claw\b', re.IGNORECASE), 'Hermes'),
      +    (re.compile(r'\bClawdBot\b', re.IGNORECASE), 'Hermes'),
      +    (re.compile(r'\bMoltBot\b', re.IGNORECASE), 'Hermes'),
      +]
      +
      +
      +def rebrand_text(text: str) -> str:
      +    """Replace OpenClaw / ClawdBot / MoltBot brand names with Hermes."""
      +    for pattern, replacement in _REBRAND_PATTERNS:
      +        text = pattern.sub(replacement, text)
      +    return text
      +
      +
       def parse_existing_memory_entries(path: Path) -> List[str]:
           if not path.exists():
               return []
      @@ -782,12 +800,13 @@ def write_overflow_entries(self, kind: str, entries: Sequence[str]) -> Optional[
               path.write_text("\n".join(entries) + "\n", encoding="utf-8")
               return path
       
      -    def copy_file(self, source: Path, destination: Path, kind: str) -> None:
      +    def copy_file(self, source: Path, destination: Path, kind: str,
      +                  transform: Optional[Any] = None) -> None:
               if not source or not source.exists():
                   return
       
               if destination.exists():
      -            if sha256_file(source) == sha256_file(destination):
      +            if not transform and sha256_file(source) == sha256_file(destination):
                       self.record(kind, source, destination, "skipped", "Target already matches source")
                       return
                   if not self.overwrite:
      @@ -797,7 +816,13 @@ def copy_file(self, source: Path, destination: Path, kind: str) -> None:
               if self.execute:
                   backup_path = self.maybe_backup(destination)
                   ensure_parent(destination)
      -            shutil.copy2(source, destination)
      +            if transform:
      +                content = read_text(source)
      +                content = transform(content)
      +                destination.write_text(content, encoding="utf-8")
      +                shutil.copystat(source, destination)
      +            else:
      +                shutil.copy2(source, destination)
                   self.record(kind, source, destination, "migrated", backup=str(backup_path) if backup_path else None)
               else:
                   self.record(kind, source, destination, "migrated", "Would copy")
      @@ -807,7 +832,7 @@ def migrate_soul(self) -> None:
               if not source:
                   self.record("soul", None, self.target_root / "SOUL.md", "skipped", "No OpenClaw SOUL.md found")
                   return
      -        self.copy_file(source, self.target_root / "SOUL.md", kind="soul")
      +        self.copy_file(source, self.target_root / "SOUL.md", kind="soul", transform=rebrand_text)
       
           def migrate_workspace_agents(self) -> None:
               source = self.source_candidate(
      @@ -821,7 +846,7 @@ def migrate_workspace_agents(self) -> None:
                   self.record("workspace-agents", source, None, "skipped", "No workspace target was provided")
                   return
               destination = self.workspace_target / WORKSPACE_INSTRUCTIONS_FILENAME
      -        self.copy_file(source, destination, kind="workspace-agents")
      +        self.copy_file(source, destination, kind="workspace-agents", transform=rebrand_text)
       
           def migrate_memory(self, source: Optional[Path], destination: Path, limit: int, kind: str) -> None:
               if not source or not source.exists():
      @@ -832,6 +857,7 @@ def migrate_memory(self, source: Optional[Path], destination: Path, limit: int,
               if not incoming:
                   self.record(kind, source, destination, "skipped", "No importable entries found")
                   return
      +        incoming = [rebrand_text(entry) for entry in incoming]
       
               existing = parse_existing_memory_entries(destination)
               merged, stats, overflowed = merge_entries(existing, incoming, limit)
      @@ -927,7 +953,7 @@ def migrate_command_allowlist(self) -> None:
       
           def load_openclaw_config(self) -> Dict[str, Any]:
               # Check current name and legacy config filenames
      -        for name in ("openclaw.json", "clawdbot.json", "moldbot.json"):
      +        for name in ("openclaw.json", "clawdbot.json", "moltbot.json"):
                   config_path = self.source_root / name
                   if config_path.exists():
                       try:
      @@ -997,7 +1023,17 @@ def migrate_messaging_settings(self, config: Optional[Dict[str, Any]] = None) ->
                   .get("workspace")
               )
               if isinstance(workspace, str) and workspace.strip():
      -            additions["MESSAGING_CWD"] = workspace.strip()
      +            ws_path = workspace.strip()
      +            # Skip if the workspace points inside the OpenClaw source directory —
      +            # that path will be stale after migration and would cause the Hermes
      +            # gateway to use the old OpenClaw workspace as its cwd, picking up
      +            # OpenClaw's AGENTS.md, MEMORY.md, etc.
      +            try:
      +                inside_source = Path(ws_path).resolve().is_relative_to(self.source_root.resolve())
      +            except (ValueError, OSError):
      +                inside_source = False
      +            if not inside_source:
      +                additions["MESSAGING_CWD"] = ws_path
       
               allowlist_path = self.source_root / "credentials" / "telegram-default-allowFrom.json"
               if allowlist_path.exists():
      @@ -1543,6 +1579,7 @@ def migrate_daily_memory(self) -> None:
               if not all_incoming:
                   self.record("daily-memory", source_dir, destination, "skipped", "No importable entries found in daily memory files")
                   return
      +        all_incoming = [rebrand_text(entry) for entry in all_incoming]
       
               existing = parse_existing_memory_entries(destination)
               merged, stats, overflowed = merge_entries(existing, all_incoming, self.memory_limit)
    • tests/hermes_cli/test_claw.py
      @@ -58,13 +58,13 @@ def test_finds_openclaw_dir(self, tmp_path):
           def test_finds_legacy_dirs(self, tmp_path):
               clawdbot = tmp_path / ".clawdbot"
               clawdbot.mkdir()
      -        moldbot = tmp_path / ".moldbot"
      -        moldbot.mkdir()
      +        moltbot = tmp_path / ".moltbot"
      +        moltbot.mkdir()
               with patch("pathlib.Path.home", return_value=tmp_path):
                   found = claw_mod._find_openclaw_dirs()
               assert len(found) == 2
               assert clawdbot in found
      -        assert moldbot in found
      +        assert moltbot in found
       
           def test_returns_empty_when_none_exist(self, tmp_path):
               with patch("pathlib.Path.home", return_value=tmp_path):
      @@ -297,7 +297,6 @@ def test_execute_with_confirmation(self, tmp_path, capsys):
                   patch.object(claw_mod, "_load_migration_module", return_value=fake_mod),
                   patch.object(claw_mod, "get_config_path", return_value=config_path),
                   patch.object(claw_mod, "prompt_yes_no", return_value=True),
      -            patch.object(claw_mod, "_offer_source_archival"),
                   patch("sys.stdin", mock_stdin),
               ):
                   claw_mod._cmd_migrate(args)
      @@ -306,43 +305,8 @@ def test_execute_with_confirmation(self, tmp_path, capsys):
               assert "Migration Results" in captured.out
               assert "Migration complete!" in captured.out
       
      -    def test_execute_offers_archival_on_success(self, tmp_path, capsys):
      -        """After successful migration, _offer_source_archival should be called."""
      -        openclaw_dir = tmp_path / ".openclaw"
      -        openclaw_dir.mkdir()
      -
      -        fake_mod = ModuleType("openclaw_to_hermes")
      -        fake_mod.resolve_selected_options = MagicMock(return_value={"soul"})
      -        fake_migrator = MagicMock()
      -        fake_migrator.migrate.return_value = {
      -            "summary": {"migrated": 3, "skipped": 0, "conflict": 0, "error": 0},
      -            "items": [
      -                {"kind": "soul", "status": "migrated", "destination": str(tmp_path / "SOUL.md")},
      -            ],
      -        }
      -        fake_mod.Migrator = MagicMock(return_value=fake_migrator)
      -
      -        args = Namespace(
      -            source=str(openclaw_dir),
      -            dry_run=False, preset="full", overwrite=False,
      -            migrate_secrets=[redacted:secret-assignment] workspace_target=None,
      -            skill_conflict="skip", yes=True,
      -        )
      -
      -        with (
      -            patch.object(claw_mod, "_find_migration_script", return_value=tmp_path / "s.py"),
      -            patch.object(claw_mod, "_load_migration_module", return_value=fake_mod),
      -            patch.object(claw_mod, "get_config_path", return_value=tmp_path / "config.yaml"),
      -            patch.object(claw_mod, "save_config"),
      -            patch.object(claw_mod, "load_config", return_value={}),
      -            patch.object(claw_mod, "_offer_source_archival") as mock_archival,
      -        ):
      -            claw_mod._cmd_migrate(args)
      -
      -        mock_archival.assert_called_once_with(openclaw_dir, True)
      -
      -    def test_dry_run_skips_archival(self, tmp_path, capsys):
      -        """Dry run should not offer archival."""
      +    def test_dry_run_does_not_touch_source(self, tmp_path, capsys):
      +        """Dry run should not modify the source directory."""
               openclaw_dir = tmp_path / ".openclaw"
               openclaw_dir.mkdir()
       
      @@ -369,11 +333,10 @@ def test_dry_run_skips_archival(self, tmp_path, capsys):
                   patch.object(claw_mod, "get_config_path", return_value=tmp_path / "config.yaml"),
                   patch.object(claw_mod, "save_config"),
                   patch.object(claw_mod, "load_config", return_value={}),
      -            patch.object(claw_mod, "_offer_source_archival") as mock_archival,
               ):
                   claw_mod._cmd_migrate(args)
       
      -        mock_archival.assert_not_called()
      +        assert openclaw_dir.is_dir()  # Source untouched
       
           def test_execute_cancelled_by_user(self, tmp_path, capsys):
               openclaw_dir = tmp_path / ".openclaw"
      @@ -506,73 +469,6 @@ def test_full_preset_enables_secrets(self, tmp_path, capsys):
               assert call_kwargs["migrate_secrets"] is True
       
       
      -# ---------------------------------------------------------------------------
      -# _offer_source_archival
      -# ---------------------------------------------------------------------------
      -
      -
      -class TestOfferSourceArchival:
      -    """Test the post-migration archival offer."""
      -
      -    def test_archives_with_auto_yes(self, tmp_path, capsys):
      -        source = tmp_path / ".openclaw"
      -        source.mkdir()
      -        (source / "workspace").mkdir()
      -        (source / "workspace" / "todo.json").write_text("{}")
      -
      -        claw_mod._offer_source_archival(source, auto_yes=True)
      -
      -        captured = capsys.readouterr()
      -        assert "Archived" in captured.out
      -        assert not source.exists()
      -        assert (tmp_path / ".openclaw.pre-migration").is_dir()
      -
      -    def test_skips_when_user_declines(self, tmp_path, capsys):
      -        source = tmp_path / ".openclaw"
      -        source.mkdir()
      -
      -        mock_stdin = MagicMock()
      -        mock_stdin.isatty.return_value = True
      -
      -        with (
      -            patch.object(claw_mod, "prompt_yes_no", return_value=False),
      -            patch("sys.stdin", mock_stdin),
      -        ):
      -            claw_mod._offer_source_archival(source, auto_yes=False)
      -
      -        captured = capsys.readouterr()
      -        assert "Skipped" in captured.out
      -        assert source.is_dir()  # Still exists
      -
      -    def test_noop_when_source_missing(self, tmp_path, capsys):
      -        claw_mod._offer_source_archival(tmp_path / "nonexistent", auto_yes=True)
      -        captured = capsys.readouterr()
      -        assert captured.out == ""  # No output
      -
      -    def test_shows_state_files(self, tmp_path, capsys):
      -        source = tmp_path / ".openclaw"
      -        source.mkdir()
      -        ws = source / "workspace"
      -        ws.mkdir()
      -        (ws / "todo.json").write_text("{}")
      -
      -        with patch.object(claw_mod, "prompt_yes_no", return_value=False):
      -            claw_mod._offer_source_archival(source, auto_yes=False)
      -
      -        captured = capsys.readouterr()
      -        assert "todo.json" in captured.out
      -
      -    def test_handles_archive_error(self, tmp_path, capsys):
      -        source = tmp_path / ".openclaw"
      -        source.mkdir()
      -
      -        with patch.object(claw_mod, "_archive_directory", side_effect=OSError("permission denied")):
      -            claw_mod._offer_source_archival(source, auto_yes=True)
      -
      -        captured = capsys.readouterr()
      -        assert "Could not archive" in captured.out
      -
      -
       # ---------------------------------------------------------------------------
       # _cmd_cleanup
       # ---------------------------------------------------------------------------
    • tests/skills/test_openclaw_migration.py
      @@ -185,6 +185,38 @@ def test_migrator_optionally_imports_supported_secrets_and_messaging_settings(tm
           assert "TELEGRAM_BOT_TOKEN=[redacted:secret-assignment]" in env_text
       
       
      +def test_messaging_cwd_skipped_when_inside_source(tmp_path: Path):
      +    """MESSAGING_CWD pointing inside the OpenClaw source dir should be skipped."""
      +    mod = load_module()
      +    source = tmp_path / ".openclaw"
      +    target = tmp_path / ".hermes"
      +    target.mkdir()
      +
      +    # Workspace path is inside the source directory
      +    ws_path = str(source / "workspace")
      +    (source / "credentials").mkdir(parents=True)
      +    (source / "openclaw.json").write_text(
      +        json.dumps({"agents": {"defaults": {"workspace": ws_path}}}),
      +        encoding="utf-8",
      +    )
      +
      +    migrator = mod.Migrator(
      +        source_root=source,
      +        target_root=target,
      +        execute=True,
      +        workspace_target=None,
      +        overwrite=False,
      +        migrate_secrets=True,
      +        output_dir=target / "migration-report",
      +        selected_options={"messaging-settings"},
      +    )
      +    migrator.migrate()
      +
      +    env_path = target / ".env"
      +    if env_path.exists():
      +        assert "MESSAGING_CWD" not in env_path.read_text(encoding="utf-8")
      +
      +
       def test_migrator_can_execute_only_selected_categories(tmp_path: Path):
           mod = load_module()
           source = tmp_path / ".openclaw"
      @@ -722,3 +754,98 @@ def test_skill_installs_cleanly_under_skills_guard():
           KNOWN_FALSE_POSITIVES = {"agent_config_mod", "python_os_environ", "hermes_config_mod"}
           for f in result.findings:
               assert f.pattern_id in KNOWN_FALSE_POSITIVES, f"Unexpected finding: {f}"
      +
      +
      +# ── rebrand_text tests ────────────────────────────────────────
      +
      +
      +def test_rebrand_text_replaces_openclaw_variants():
      +    mod = load_module()
      +    assert mod.rebrand_text("OpenClaw prefers Python 3.11") == "Hermes prefers Python 3.11"
      +    assert mod.rebrand_text("I told Open Claw to use dark mode") == "I told Hermes to use dark mode"
      +    assert mod.rebrand_text("Open-Claw config is great") == "Hermes config is great"
      +    assert mod.rebrand_text("openclaw should always respond concisely") == "Hermes should always respond concisely"
      +    assert mod.rebrand_text("OPENCLAW uses tools well") == "Hermes uses tools well"
      +
      +
      +def test_rebrand_text_replaces_legacy_bot_names():
      +    mod = load_module()
      +    assert mod.rebrand_text("ClawdBot remembers my timezone") == "Hermes remembers my timezone"
      +    assert mod.rebrand_text("clawdbot prefers tabs") == "Hermes prefers tabs"
      +    assert mod.rebrand_text("MoltBot was configured for Spanish") == "Hermes was configured for Spanish"
      +    assert mod.rebrand_text("moltbot uses Python") == "Hermes uses Python"
      +
      +
      +def test_rebrand_text_preserves_unrelated_content():
      +    mod = load_module()
      +    text = "User prefers dark mode and lives in Las Vegas"
      +    assert mod.rebrand_text(text) == text
      +
      +
      +def test_rebrand_text_handles_multiple_replacements():
      +    mod = load_module()
      +    text = "OpenClaw said to ask ClawdBot about MoltBot settings"
      +    assert mod.rebrand_text(text) == "Hermes said to ask Hermes about Hermes settings"
      +
      +
      +def test_migrate_memory_rebrands_entries(tmp_path):
      +    mod = load_module()
      +    source_root = tmp_path / "openclaw"
      +    source_root.mkdir()
      +    workspace = source_root / "workspace"
      +    workspace.mkdir()
      +    memory_md = workspace / "MEMORY.md"
      +    memory_md.write_text(
      +        "# Memory\n\n- OpenClaw should use Python 3.11\n- ClawdBot prefers dark mode\n",
      +        encoding="utf-8",
      +    )
      +
      +    target_root = tmp_path / "hermes"
      +    target_root.mkdir()
      +    (target_root / "memories").mkdir()
      +
      +    migrator = mod.Migrator(
      +        source_root=source_root,
      +        target_root=target_root,
      +        execute=True,
      +        workspace_target=None,
      +        overwrite=False,
      +        migrate_secrets=[redacted:secret-assignment]
      +        output_dir=tmp_path / "report",
      +        selected_options={"memory"},
      +    )
      +    migrator.migrate()
      +
      +    result = (target_root / "memories" / "MEMORY.md").read_text(encoding="utf-8")
      +    assert "OpenClaw" not in result
      +    assert "ClawdBot" not in result
      +    assert "Hermes" in result
      +
      +
      +def test_migrate_soul_rebrands_content(tmp_path):
      +    mod = load_module()
      +    source_root = tmp_path / "openclaw"
      +    source_root.mkdir()
      +    workspace = source_root / "workspace"
      +    workspace.mkdir()
      +    soul_md = workspace / "SOUL.md"
      +    soul_md.write_text("You are OpenClaw, an AI assistant made by SparkLab.", encoding="utf-8")
      +
      +    target_root = tmp_path / "hermes"
      +    target_root.mkdir()
      +
      +    migrator = mod.Migrator(
      +        source_root=source_root,
      +        target_root=target_root,
      +        execute=True,
      +        workspace_target=None,
      +        overwrite=False,
      +        migrate_secrets=[redacted:secret-assignment]
      +        output_dir=tmp_path / "report",
      +        selected_options={"soul"},
      +    )
      +    migrator.migrate()
      +
      +    result = (target_root / "SOUL.md").read_text(encoding="utf-8")
      +    assert "OpenClaw" not in result
      +    assert "You are Hermes" in result
    • website/docs/guides/migrate-from-openclaw.md
      @@ -23,7 +23,7 @@ hermes claw migrate --preset full --yes
       
       The migration always shows a full preview of what will be imported before making any changes. Review the list, then confirm to proceed.
       
      -Reads from `~/.openclaw/` by default. Legacy `~/.clawdbot/` or `~/.moldbot/` directories are detected automatically. Same for legacy config filenames (`clawdbot.json`, `moldbot.json`).
      +Reads from `~/.openclaw/` by default. Legacy `~/.clawdbot/` or `~/.moltbot/` directories are detected automatically. Same for legacy config filenames (`clawdbot.json`, `moltbot.json`).
       
       ## Options
       
      @@ -234,7 +234,7 @@ The migration resolves all three formats. For env templates and SecretRef object
       
       ### "OpenClaw directory not found"
       
      -The migration checks `~/.openclaw/`, then `~/.clawdbot/`, then `~/.moldbot/`. If your installation is elsewhere, use `--source /path/to/your/openclaw`.
      +The migration checks `~/.openclaw/`, then `~/.clawdbot/`, then `~/.moltbot/`. If your installation is elsewhere, use `--source /path/to/your/openclaw`.
       
       ### "No provider API keys found"
       
    • website/docs/reference/cli-commands.md
      @@ -660,7 +660,7 @@ hermes insights [--days N] [--source platform]
       hermes claw migrate [options]
       ```
       
      -Migrate your OpenClaw setup to Hermes. Reads from `~/.openclaw` (or a custom path) and writes to `~/.hermes`. Automatically detects legacy directory names (`~/.clawdbot`, `~/.moldbot`) and config filenames (`clawdbot.json`, `moldbot.json`).
      +Migrate your OpenClaw setup to Hermes. Reads from `~/.openclaw` (or a custom path) and writes to `~/.hermes`. Automatically detects legacy directory names (`~/.clawdbot`, `~/.moltbot`) and config filenames (`clawdbot.json`, `moltbot.json`).
       
       | Option | Description |
       |--------|-------------|

1 PR closed without merging

From the close comments: merged via another change 1

  • #8192 fix(migration): don't auto-archive OpenClaw source directory · merged via another change → #8210
Closed issues · 1
  • Closed#8188Migration should not auto-archive source directory; needs coexistence guidance
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.00 means 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 Lab

At a glance

Merged PRs
#8210
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.

For your agent

The same facts, typed.