agentsclimarketplace

Openclaw to hermes migration

Skill geekjourneyx/openclaw-to-hermes-migration

Agent skill for safely migrating OpenClaw workspaces to Hermes Agent with providers, gateways, cron jobs, and rollback

Install
npx -y skills add geekjourneyx/openclaw-to-hermes-migration

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 3 stars3 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Use this skill whenever the user wants to migrate from OpenClaw to Hermes Agent, upgrade an OpenClaw workspace into Hermes, preserve skills/memory/cron/messaging/provider settings, debug Feishu or Weixin gateway issues after migration, or validate that Hermes is ready to replace OpenClaw. This skill is especially relevant for requests mentioning OpenClaw, Hermes Agent, `hermes claw migrate`, Feishu/Lark, Weixin/WeChat, model providers, API keys, cron jobs, gateway services, or “确保迁移成功”.

SKILL.md

21.8 KB, as published. Nobody here has run it

OpenClaw to Hermes Migration

This skill turns an OpenClaw installation into a verified Hermes Agent installation without losing identity, memory, skills, model settings, messaging access, or scheduled automation intent.

Use a conservative migration posture: freeze the source, dry-run first, migrate user data before secrets, validate each capability, then start Hermes gateway only after OpenClaw gateway is stopped.

Core Principles

  • Treat migration as state transfer, not file copying. Preserve behavior: identity, memory, execution defaults, skills, messaging, model auth, and automation.
  • Do not print secrets. Report secret presence as booleans or key names only.
  • Keep OpenClaw available until Hermes passes acceptance checks.
  • Do not run two gateways against the same bot/app/session token.
  • Prefer --preset user-data first. Migrate secrets only after the dry-run proves it will not duplicate or overwrite unrelated state.
  • Keep the original workspace path during cutover if memories, skills, or cron prompts reference it.

Inputs To Identify

Ask or infer:

  • OpenClaw home, usually ~/.openclaw.
  • Hermes home, usually ~/.hermes.
  • Target workspace, often ~/.openclaw/workspace during initial cutover.
  • Required messaging platforms: Feishu/Lark, Weixin/WeChat, Telegram, Discord, etc.
  • Desired model provider: z.ai/BigModel, OpenRouter, Gemini, Anthropic, custom OpenAI-compatible endpoint, or another supported provider.
  • Whether secrets may be migrated or must be manually re-entered.
  • Whether cron jobs should be recreated immediately, paused, or left archived.

Bundled Resources

  • Read references/migration-session-summary.md only when you need a concrete prior migration example, known Hermes/OpenClaw paths, Feishu/z.ai troubleshooting notes, or a record of commands that worked in one real migration.
  • Use evals/evals.json when changing this skill to check that the SOP still covers safe migration, Feishu debugging, provider replacement, and cron generalization.

Phase 1: Freeze OpenClaw

Before migrating, stop active OpenClaw writers. Runtime files can otherwise change during checksum capture.

systemctl --user is-active openclaw-gateway.service || true
systemctl --user stop openclaw-gateway.service
lsof +D ~/.openclaw 2>/dev/null | head -80 || true

If OpenClaw is not systemd-managed, inspect processes:

ps -eo pid,ppid,etime,cmd | grep -Ei 'openclaw|weixin|wechat|feishu|hermes' | grep -v grep || true

Only stop confirmed OpenClaw runtime components. Do not kill generic node, python, or shell processes without proving they own OpenClaw files.

Capture a manifest:

mkdir -p /tmp/openclaw-migration
find ~/.openclaw -type f | sort > /tmp/openclaw-migration/source-files.txt
find ~/.openclaw -type f -print0 | sort -z | xargs -0 sha256sum > /tmp/openclaw-migration/source-sha256.txt

Capture a non-secret summary:

{
  echo "date_utc=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
  echo "openclaw_size=$(du -sh ~/.openclaw | awk '{print $1}')"
  echo "workspace_files=$(find ~/.openclaw/workspace -type f 2>/dev/null | wc -l)"
  echo "main_sessions_glob=~/.openclaw/agents/main/sessions/*.jsonl"
  echo "main_sessions=$(find ~/.openclaw/agents/main/sessions -type f -name '*.jsonl' 2>/dev/null | wc -l)"
  echo "global_skills=$(find ~/.openclaw/skills -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l)"
  echo "workspace_skills=$(find ~/.openclaw/workspace/skills -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l)"
  echo "cron_jobs=$(jq '.jobs | length' ~/.openclaw/cron/jobs.json 2>/dev/null || echo 0)"
  jq -c '{top_keys: keys, channels: (.channels // {} | keys), providers: (.models.providers // {} | keys), default_workspace: .agents.defaults.workspace}' ~/.openclaw/openclaw.json
} > /tmp/openclaw-migration/source-summary.txt
touch /tmp/openclaw-migration/source-summary.txt
find ~/.openclaw -type f -newer /tmp/openclaw-migration/source-summary.txt -print

Proceed only when the newer-file check is empty, or when the user explicitly accepts online migration risk.

Phase 2: Install Hermes

Install from the official script:

curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash
command -v hermes
hermes doctor

If the installer exits after creating Hermes because setup wizard cannot open /dev/tty, verify with command -v hermes and hermes doctor. A no-TTY wizard failure is not a blocker if the CLI, config, venv, and directories exist.

Phase 3: Dry-Run Migration

Run a safe preview:

hermes claw migrate \
  --source ~/.openclaw \
  --workspace-target ~/.openclaw/workspace \
  --preset user-data \
  --skill-conflict rename \
  --overwrite \
  --dry-run

Inspect:

  • Would migrate: soul, workspace-agents, memory, user-profile, model-config, agent-config, skills, custom providers.
  • Skipped: messaging settings, raw config, sensitive runtime data, cron jobs, provider keys.
  • Conflicts: should be resolved by --overwrite for template files and --skill-conflict rename for duplicate skills.

If Hermes reports OpenClaw PIDs during dry-run, verify independently:

ps -fp <pid-list> || true
systemctl --user is-active openclaw-gateway.service || true
lsof +D ~/.openclaw 2>/dev/null | head -80 || true

Migration detector warnings can be transient if it matches command lines containing openclaw. Do not ignore the warning until service state and open files are checked.

Phase 4: Execute User-Data Migration

Execute only after dry-run is clean:

hermes claw migrate \
  --source ~/.openclaw \
  --workspace-target ~/.openclaw/workspace \
  --preset user-data \
  --skill-conflict rename \
  --overwrite \
  --yes

Validate:

test -s ~/.hermes/SOUL.md
test -s ~/.hermes/memories/MEMORY.md
test -s ~/.hermes/memories/USER.md
test -d ~/.hermes/skills/openclaw-imports
hermes skills list
hermes doctor

Review the migration report:

find ~/.hermes/migration/openclaw -maxdepth 3 -type f | sort | tail -80
sed -n '1,180p' ~/.hermes/migration/openclaw/<timestamp>/summary.md

Phase 5: Provider Migration

After user data is migrated, wire the desired provider. Avoid assuming the source has any particular provider, and avoid leaving obsolete provider entries that the user no longer wants.

General Provider Cleanup Pattern

Use this pattern when the user wants to replace one provider with another:

  1. Identify source providers and credentials without printing values.
  2. Confirm the target provider and model ID.
  3. Back up ~/.hermes/.env and ~/.hermes/config.yaml.
  4. Add the target provider key to ~/.hermes/.env.
  5. Remove only provider env vars and custom_providers entries that the user explicitly wants removed.
  6. Update model.default, model.provider, model.base_url, and model.api_key to the target provider.
  7. Validate with hermes config check, hermes doctor, and a one-shot hermes chat smoke test.

Non-secret discovery commands:

jq '{default_model: .agents.defaults.model, provider_names: (.models.providers // {} | keys)}' ~/.openclaw/openclaw.json
python3 - <<'PY'
import json
from pathlib import Path
p = Path("~/.openclaw/agents/main/agent/auth-profiles.json").expanduser()
if p.exists():
    data = json.loads(p.read_text())
    print("auth_profiles=", sorted(data.get("profiles", {}).keys()))
PY

Generic OpenAI-Compatible Provider Template

Use this when the target provider exposes an OpenAI-compatible chat completions endpoint. Replace placeholder values and remove old providers only when explicitly requested:

TARGET_PROVIDER="provider-name"
TARGET_MODEL="provider-name/model-id"
TARGET_BASE_URL="https://provider.example.com/v1"
TARGET_API_KEY_ENV="PROVIDER_API_KEY"
export TARGET_PROVIDER TARGET_MODEL TARGET_BASE_URL TARGET_API_KEY_ENV

python3 - <<'PY'
import os
from pathlib import Path
import yaml

provider = os.environ["TARGET_PROVIDER"]
model = os.environ["TARGET_MODEL"]
base_url = os.environ["TARGET_BASE_URL"]
api_key_env = os.environ["TARGET_API_KEY_ENV"]
config = Path("~/.hermes/config.yaml").expanduser()
env_file = Path("~/.hermes/.env").expanduser()
backup = config.with_name(config.name + f".bak-before-{provider}")

if not config.exists():
    raise SystemExit("Hermes config.yaml not found; run hermes doctor/setup first.")
env_has_key = os.environ.get(api_key_env) or (
    env_file.exists() and any(line.startswith(api_key_env + "=") for line in env_file.read_text().splitlines())
)
if not env_has_key:
    raise SystemExit(f"{api_key_env} is not set; add it to ~/.hermes/.env or export it before configuring.")
if not backup.exists():
    backup.write_text(config.read_text())

cfg = yaml.safe_load(config.read_text()) or {}
cfg.setdefault("model", {})
cfg["model"].update({
    "default": model,
    "provider": "auto",
    "base_url": base_url,
    "api_key": "${" + api_key_env + "}",
})

remove_provider_names = set()  # Fill only with user-approved removals.
providers, found = [], False
for item in cfg.get("custom_providers") or []:
    if not isinstance(item, dict) or item.get("name") in remove_provider_names:
        continue
    if item.get("name") == provider:
        item.update({"base_url": base_url, "api_key": "${" + api_key_env + "}", "api_mode": "chat_completions"})
        found = True
    providers.append(item)
if not found:
    providers.append({"name": provider, "base_url": base_url, "api_key": "${" + api_key_env + "}", "api_mode": "chat_completions"})
cfg["custom_providers"] = providers
config.write_text(yaml.safe_dump(cfg, allow_unicode=True, sort_keys=False))
print(f"{provider} configured.")
PY

Example: z.ai / BigModel Domestic Endpoint

If OpenClaw stores z.ai auth profiles:

python3 - <<'PY'
import json
from pathlib import Path

src = Path("~/.openclaw/agents/main/agent/auth-profiles.json").expanduser()
env = Path("~/.hermes/.env").expanduser()
backup = env.with_name(env.name + ".bak-before-zai")

if not src.exists():
    raise SystemExit("No OpenClaw auth-profiles.json found; set ZAI_API_KEY manually.")
data = json.loads(src.read_text())
profile = data.get("profiles", {}).get("zai:default")
if not profile or not profile.get("key"):
    raise SystemExit("No zai:default key found; set ZAI_API_KEY manually.")
key = profile["key"]

if env.exists() and not backup.exists():
    backup.write_text(env.read_text())

lines = env.read_text().splitlines() if env.exists() else []
out, seen = [], False
for line in lines:
    if line.startswith("ZAI_API_KEY="):
        out.append(f"ZAI_API_KEY={key}")
        seen = True
    else:
        out.append(line)
if not seen:
    out.append(f"ZAI_API_KEY={key}")
env.write_text("\n".join(out).rstrip() + "\n")
print("ZAI_API_KEY configured.")
PY

Update ~/.hermes/config.yaml:

python3 - <<'PY'
from pathlib import Path
import yaml

path = Path("~/.hermes/config.yaml").expanduser()
backup = path.with_name(path.name + ".bak-before-zai")

if not path.exists():
    raise SystemExit("Hermes config.yaml not found; run hermes doctor/setup first.")
if not backup.exists():
    backup.write_text(path.read_text())

cfg = yaml.safe_load(path.read_text()) or {}
cfg.setdefault("model", {})
cfg["model"]["default"] = "zai/glm-5-turbo"
cfg["model"]["provider"] = "auto"
cfg["model"]["base_url"] = "https://open.bigmodel.cn/api/coding/paas/v4"
cfg["model"]["api_key"] = "${ZAI_API_KEY}"

remove_provider_names = set()  # Example: {"old-provider"} only when user asked to remove it.
providers, found = [], False
for item in cfg.get("custom_providers") or []:
    if not isinstance(item, dict):
        continue
    if item.get("name") in remove_provider_names:
        continue
    if item.get("name") == "zai":
        item["base_url"] = "https://open.bigmodel.cn/api/coding/paas/v4"
        item["api_key"] = "${ZAI_API_KEY}"
        item["api_mode"] = "chat_completions"
        found = True
    providers.append(item)
if not found:
    providers.append({
        "name": "zai",
        "base_url": "https://open.bigmodel.cn/api/coding/paas/v4",
        "api_key": "${ZAI_API_KEY}",
        "api_mode": "chat_completions",
    })
cfg["custom_providers"] = providers
path.write_text(yaml.safe_dump(cfg, allow_unicode=True, sort_keys=False))
print("z.ai provider configured.")
PY

Validate:

hermes config check
hermes doctor
timeout 90 hermes chat -q '只回复 OK' --provider zai -m zai/glm-5-turbo -Q --max-turns 1 --ignore-rules

Expected: hermes doctor shows ✓ Z.AI / GLM, and the smoke test returns OK.

If the user asks to remove a specific old provider, remove that provider explicitly. For example, to remove old-provider, delete its env var from ~/.hermes/.env and add the provider name to remove_provider_names. Do not remove providers just because they were present in the source.

Phase 6: Feishu / Lark Migration

Hermes supports Feishu/Lark gateway even if hermes gateway --help text is incomplete. Prefer WebSocket mode to avoid public webhook setup.

Migrate OpenClaw Feishu config. This script is intentionally safe to run when Feishu was not configured; it exits without changing Hermes.

python3 - <<'PY'
import json
from pathlib import Path

openclaw_path = Path("~/.openclaw/openclaw.json").expanduser()
env = Path("~/.hermes/.env").expanduser()
backup = env.with_name(env.name + ".bak-before-feishu")

if not openclaw_path.exists():
    raise SystemExit("No ~/.openclaw/openclaw.json found; skipping Feishu migration.")
openclaw = json.loads(openclaw_path.read_text())
feishu = (openclaw.get("channels") or {}).get("feishu")
if not isinstance(feishu, dict) or not feishu.get("appId") or not feishu.get("appSecret"):
    raise SystemExit("No complete OpenClaw Feishu config found; skipping Feishu migration.")

allow_path = Path("~/.openclaw/credentials/feishu-default-allowFrom.json").expanduser()
allow = json.loads(allow_path.read_text()) if allow_path.exists() else {}
allowed = allow.get("allowFrom", [])

updates = {
    "FEISHU_APP_ID": feishu.get("appId", ""),
    "FEISHU_APP_SECRET": feishu.get("appSecret", ""),
    "FEISHU_DOMAIN": "feishu",
    "FEISHU_CONNECTION_MODE": feishu.get("connectionMode", "websocket"),
    "FEISHU_GROUP_POLICY": feishu.get("groupPolicy", "allowlist"),
}
if isinstance(allowed, list) and allowed:
    updates["FEISHU_ALLOWED_USERS"] = ",".join(str(x) for x in allowed)

if env.exists() and not backup.exists():
    backup.write_text(env.read_text())
lines = env.read_text().splitlines() if env.exists() else []
out, seen = [], set()
for line in lines:
    if "=" in line and not line.lstrip().startswith("#"):
        key = line.split("=", 1)[0]
        if key in updates:
            out.append(f"{key}={updates[key]}")
            seen.add(key)
            continue
    out.append(line)
for key, value in updates.items():
    if key not in seen and value:
        out.append(f"{key}={value}")
env.write_text("\n".join(out).rstrip() + "\n")
print("Feishu config migrated.")
PY

Start in foreground for first test:

hermes gateway run --accept-hooks

Then install as a service:

hermes gateway install
hermes gateway start
hermes gateway status
journalctl --user -u hermes-gateway -f

If messages arrive but no reply:

journalctl --user -u hermes-gateway --since '10 minutes ago' --no-pager

Common Feishu issues:

  • Unauthorized user: <id>: add that sender ID to FEISHU_ALLOWED_USERS, then hermes gateway restart.
  • Access denied ... im:chat:readonly: open the Feishu developer console and grant im:chat:readonly or equivalent chat read scope.
  • Group message ignored: the bot usually requires an explicit @mention in groups; check FEISHU_GROUP_POLICY.
  • Card approval button errors: enable interactive card capability and subscribe to card.action.trigger.

Phase 7: Weixin / WeChat Migration

Hermes supports personal Weixin via iLink Bot API. It is different from WeCom.

If OpenClaw has account files, migrate the first account file. This script exits without changing Hermes when no account exists.

python3 - <<'PY'
import json
from pathlib import Path

accounts_dir = Path("~/.openclaw/openclaw-weixin/accounts").expanduser()
env = Path("~/.hermes/.env").expanduser()
backup = env.with_name(env.name + ".bak-before-weixin")

if not accounts_dir.exists():
    raise SystemExit("No OpenClaw Weixin accounts directory found; skipping Weixin migration.")
accounts = sorted(accounts_dir.glob("*-im-bot.json"))
if not accounts:
    raise SystemExit("No OpenClaw Weixin account files found; skipping Weixin migration.")

account_file = accounts[0]
data = json.loads(account_file.read_text())
if not data.get("token") or not data.get("baseUrl"):
    raise SystemExit(f"{account_file} is missing token/baseUrl; skipping Weixin migration.")
updates = {
    "WEIXIN_ACCOUNT_ID": account_file.stem,
    "WEIXIN_TOKEN": data.get("token", ""),
    "WEIXIN_BASE_URL": data.get("baseUrl", ""),
    "WEIXIN_DM_POLICY": "open",
    "WEIXIN_GROUP_POLICY": "disabled",
}

if env.exists() and not backup.exists():
    backup.write_text(env.read_text())
lines = env.read_text().splitlines() if env.exists() else []
out, seen = [], set()
for line in lines:
    if "=" in line and not line.lstrip().startswith("#"):
        key = line.split("=", 1)[0]
        if key in updates:
            out.append(f"{key}={updates[key]}")
            seen.add(key)
            continue
    out.append(line)
for key, value in updates.items():
    if key not in seen and value:
        out.append(f"{key}={value}")
env.write_text("\n".join(out).rstrip() + "\n")
print("Weixin config migrated.")
PY

If the session is expired, run:

hermes gateway setup

Select Weixin and scan the QR code.

Phase 8: Cron Jobs

OpenClaw cron is often archived rather than directly migrated. Never assume jobs[0] is the only job. Inspect all archived jobs first:

jq '.jobs[] | {id, name, enabled, schedule, delivery, state, payload_keys: (.payload | keys)}' ~/.hermes/migration/openclaw/<timestamp>/archive/cron-store/jobs.json

Generate reviewable recreate commands for every job. This does not execute anything:

CRON_ARCHIVE=~/.hermes/migration/openclaw/<timestamp>/archive/cron-store/jobs.json python3 - <<'PY'
import json, os, shlex
from pathlib import Path

path = Path(os.environ["CRON_ARCHIVE"]).expanduser()
data = json.loads(path.read_text())

def schedule_to_cli(value):
    if isinstance(value, str) and value.strip():
        return value.strip()
    if isinstance(value, dict):
        for key in ("expression", "cron", "schedule"):
            if isinstance(value.get(key), str) and value[key].strip():
                return value[key].strip()
        minutes = value.get("intervalMinutes") or value.get("interval_minutes")
        if minutes:
            return f"every {int(minutes)}m"
        seconds = value.get("intervalSeconds") or value.get("interval_seconds")
        if seconds and int(seconds) % 60 == 0:
            return f"every {int(seconds) // 60}m"
    return None

def prompt_from(job):
    payload = job.get("payload") or {}
    for key in ("message", "prompt", "task", "instruction", "text"):
        value = payload.get(key) or job.get(key)
        if isinstance(value, str) and value.strip():
            return value.strip()
    return None

for index, job in enumerate(data.get("jobs") or [], 1):
    schedule = schedule_to_cli(job.get("schedule"))
    prompt = prompt_from(job)
    if not schedule or not prompt:
        print(f"# SKIP job {index}: missing schedule or prompt; inspect manually")
        continue
    name = job.get("name") or job.get("id") or f"openclaw-cron-{index}"
    delivery = job.get("delivery") or job.get("deliver") or "local"
    if not isinstance(delivery, str) or delivery in {"", "auto", "origin"}:
        delivery = "local"
    skills = job.get("skills") or (job.get("payload") or {}).get("skills") or []
    if isinstance(skills, str):
        skills = [skills]
    args = ["hermes", "cron", "create", schedule, prompt, "--name", str(name), "--deliver", delivery]
    for skill in skills:
        args += ["--skill", str(skill)]
    print(shlex.join(args))
    print("# After creation, immediately run: hermes cron pause <new-job-id>")
PY

Recreate selected jobs one by one, then pause each created job before enabling the next. Use the id printed by hermes cron create; if the CLI output is unclear, run hermes cron list --all and pause the new id before creating more jobs.

hermes cron create "<schedule>" "<prompt>" --name "<name>" --deliver local
hermes cron list --all
hermes cron pause <new-job-id>
hermes cron list --all

Pause cron until model auth and delivery channel are proven.

Acceptance Checklist

Migration is not complete until:

  • hermes doctor has no blocking install/config issues.
  • SOUL.md, MEMORY.md, and USER.md exist in ~/.hermes.
  • hermes skills list shows imported skills under openclaw-imports.
  • The desired provider passes a real smoke test.
  • The messaging gateway connects and responds to a real user message.
  • Old OpenClaw gateway is stopped.
  • Cron jobs are either recreated and paused, or intentionally deferred with a written reason.
  • The migration report under ~/.hermes/migration/openclaw/<timestamp>/ is reviewed.

Rollback

Keep rollback simple:

hermes gateway stop || true
systemctl --user start openclaw-gateway.service

If Hermes state must be set aside:

mv ~/.hermes ~/.hermes.failed-$(date -u +%Y%m%dT%H%M%SZ)

Do not run OpenClaw cleanup until Hermes has survived at least one real work session.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.