{"slug":"migrate-from-openclaw","title":"migrate-from-openclaw","summary":"Migrate from OpenClaw to NanoClaw v2. Detects an existing OpenClaw installation, extracts identity, channel credentials, scheduled tasks, and other config, then guides interactive migration. Triggers on \"migrate from openclaw\", \"openclaw migration\", \"import from openclaw\".","platform":"Claude","tags":[],"authorName":"LLM Mart","authorSlug":"llm-mart","score":0,"source":"github","price":null,"verified":false,"createdAt":"2026-08-24T05:37:20.867903Z","repo":{"url":"https://github.com/nanocoai/nanoclaw","stars":30841,"forks":12823,"license":"MIT","updatedAt":"2026-09-23T17:25:39Z"},"bodyHtml":"<hr>\n<h2>name: migrate-from-openclaw\ndescription: Migrate from OpenClaw to NanoClaw v2. Detects an existing OpenClaw installation, extracts identity, channel credentials, scheduled tasks, and other config, then guides interactive migration. Triggers on \"migrate from openclaw\", \"openclaw migration\", \"import from openclaw\".</h2>\n<h1>Migrate from OpenClaw</h1>\n<p>Guide the user through migrating their OpenClaw installation into NanoClaw v2.\nThis is a conversation, not a batch job. Read OpenClaw state, discuss it with\nthe user, decide together what to bring over and where it belongs in v2's\nentity model, and show proposed changes before applying.</p>\n<p><strong>Principle:</strong> Never silently copy data. Read it, explain it, place it, then\napply. Credentials are masked when displayed (first 4 + <code>...</code> + last 4). Make\njudgment calls about what's core vs. reference material.</p>\n<p><strong>UX:</strong> Use <code>AskUserQuestion</code> for multiple-choice only. Use plain text for\nfree-form input. Don't dump raw data — summarize and explain conversationally.</p>\n<h2>What this skill changes (conformance)</h2>\n<p>This skill drives existing NanoClaw entry points (<code>setup/index.ts --step register</code>, <code>scripts/init-first-agent.ts</code>, the <code>onecli</code> CLI) and copies a few\nfiles in (workspace markdown, OpenClaw skills, and its own transform module +\ntest). It makes no code-level reach-in into core. Its integration assumptions\nabout v2 are guarded by <code>scripts/transform.test.ts</code>, which is copied into the\nproject's <code>scripts/</code> test tree on apply (Phase 8) so vitest runs it against the\ncomposed install. <code>REMOVE.md</code> reverses every file the skill copies.</p>\n<h2>v2 architecture the migration targets</h2>\n<p>OpenClaw and NanoClaw v2 differ structurally. Keep these in mind throughout:</p>\n<ul>\n<li><strong>Entity model.</strong> v2's central DB (<code>data/v2.db</code>) holds <code>users</code>,\n<code>user_roles</code>, <code>agent_groups</code>, <code>messaging_groups</code>, and the\n<code>messaging_group_agents</code> wiring between them. There is no <code>store/messages.db</code>\nand no <code>scheduled_tasks</code> table.</li>\n<li><strong>Container isolation.</strong> Each agent group runs in its own Linux container.\nAn OpenClaw \"agent\" maps to a v2 <em>agent group</em> (workspace + memory +\nCLAUDE.md); an OpenClaw chat/group maps to a v2 <em>messaging group</em>; the wiring\nrow connects them.</li>\n<li><strong>Standing instructions vs memory.</strong> Per-group role, personality, and\nbehavior live in <code>groups/&lt;folder&gt;/instructions.prepend.md</code>. Durable facts\nlive under <code>groups/&lt;folder&gt;/memory/</code>. The provider project document is\ncomposed at spawn and must not be edited.</li>\n<li><strong>Credentials.</strong> Container-facing API credentials (Anthropic, OpenAI, …) are\nheld in the OneCLI Agent Vault and injected per request — never in container\nenv vars. Host-side channel tokens (Telegram/Discord/Slack bot tokens) stay\nin <code>.env</code>; the NanoClaw host process reads them to connect to the platform.</li>\n<li><strong>Access control.</strong> Per messaging group <code>unknown_sender_policy</code> plus\n<code>user_roles</code> (owner/admin) and <code>agent_group_members</code> — not a JSON allowlist\nfile.</li>\n<li><strong>Scheduled tasks.</strong> A task is a <code>messages_in</code> row (<code>kind='task'</code>) in a\nsession's <code>inbound.db</code>, carrying a cron <code>recurrence</code> and a <code>process_after</code>\ntimestamp. The agent creates them via its <code>schedule_task</code> MCP tool.</li>\n</ul>\n<h2>Migration State File</h2>\n<p>Create <code>migration-state.md</code> in the project root at the start of Phase 0. Update\nit after each phase. It's the single source of truth — if context is lost,\nre-read it to recover decisions and progress. Re-read it before starting any\nphase.</p>\n<p>Sections to maintain:</p>\n<ul>\n<li><strong>Progress</strong> — checkbox list of phases (Phase 0–8)</li>\n<li><strong>Discovery</strong> — STATE_DIR, IDENTITY_NAME, channels, groups (with v2\nplatform_id mappings), workspace files, cron count, MCP servers</li>\n<li><strong>Decisions</strong> — assistant_name, shared-vs-separate, primary owner agent</li>\n<li><strong>Owner &amp; Primary Agent</strong> — user id, role, agent group folder</li>\n<li><strong>Registered Groups</strong> — table: folder, platform_id, channel, session_mode</li>\n<li><strong>Credentials</strong> — table: credential, destination (vault / .env), status</li>\n<li><strong>Settings Migrated</strong> — timezone, container timeout</li>\n<li><strong>Identity &amp; Memory</strong> — prepend and memory paths created for each group</li>\n<li><strong>Scheduled Tasks</strong> — table: original_id, name, mapped schedule, status</li>\n<li><strong>Deferred / Not Applicable</strong> — unsupported channels, OpenClaw-only features</li>\n</ul>\n<p>Keep it factual and terse. Delete it at the end of Phase 8 (or offer to keep it\nas a record).</p>\n<h2>Phase 0: Discovery</h2>\n<p>Run the discovery script to find and summarize the OpenClaw installation:</p>\n<pre><code>pnpm exec tsx ${CLAUDE_SKILL_DIR}/scripts/discover-openclaw.ts\n</code></pre>\n<p>If the user specifies a custom path, pass <code>--state-dir &lt;path&gt;</code>.</p>\n<p>Parse the status block. Key fields: STATUS, STATE_DIR, CHANNELS,\nWORKSPACE_FILES, DAILY_MEMORY_FILES, SKILL_COUNT, SKILLS, CRON_JOBS,\nMCP_SERVERS, IDENTITY_NAME, AGENT_COUNT, AGENT_IDS, GROUPS (each formatted\n<code>channel:id(name)=&gt;v2_platform_id</code> — the right-hand value is what to pass as\n<code>--platform-id</code> to register).</p>\n<p><strong>Sanity-check the output.</strong> The script detects known structures but can miss\ndata if OpenClaw's format changed. Check <code>CONFIG_TOP_KEYS</code> and\n<code>CONFIG_CHANNEL_KEYS</code> — if you see keys it didn't report on, read that section\nof the config with the Read tool. Check <code>STATE_DIR_CONTENTS</code> for directories it\ndoesn't scan.</p>\n<p><strong>If STATUS=not_found:</strong> Tell the user no OpenClaw install was detected at the\nstandard locations (<code>~/.openclaw</code>, <code>~/.clawdbot</code>). Ask for a custom path; if\nnone, exit.</p>\n<p><strong>If STATUS=found:</strong> Present a human-readable summary (identity name, workspace\nfiles, channels and which v2 supports, daily memory count, skills, cron count,\nMCP servers, agent count). Then paraphrase the key architectural differences\nfrom the section above — don't dump it as a table.</p>\n<p>AskUserQuestion: \"Ready to start migrating? I'll go through each area one at a\ntime.\"</p>\n<ol>\n<li><strong>Yes, let's go</strong> — proceed to Phase 1</li>\n<li><strong>Tell me more</strong> — explain any area they ask about</li>\n<li><strong>Skip migration</strong> — exit</li>\n</ol>\n<h2>Phase 1: Agents, Groups, and Shared vs Separate</h2>\n<p><strong>Decide this before identity/memory</strong> — it determines where files go.</p>\n<p><strong>OpenClaw model:</strong> all groups routed to one agent share a workspace\n(SOUL/MEMORY/IDENTITY) and personality; only the session is per-group.</p>\n<p><strong>v2 model:</strong> each agent group is a separate container with its own filesystem,\nstanding instructions, and <code>memory/</code> tree. Multiple messaging groups wired to\nthe same agent group share that state. There is no <code>groups/global/</code>.</p>\n<p>AskUserQuestion: \"In OpenClaw your groups shared one personality and memory. In\nv2 each agent group is separate. How do you want to handle this?\"</p>\n<ol>\n<li><strong>Shared identity (recommended if it was one bot)</strong> — apply the same core\nidentity to each selected group's <code>instructions.prepend.md</code>; keep group\nfacts in each group's memory tree.</li>\n<li><strong>Fully separate</strong> — each group gets independent memory and instructions; no\nshared base edit.</li>\n<li><strong>Just the primary agent for now</strong> — set one agent up; add others later.</li>\n</ol>\n<p>Remember this choice for Phase 3.</p>\n<h3>Confirm the assistant name</h3>\n<p><code>IDENTITY_NAME</code> from discovery is the OpenClaw name. Ask: \"Your OpenClaw\nassistant was named <code>&lt;IDENTITY_NAME&gt;</code>. Keep it in v2?\" If empty, ask them to\nchoose (default: \"Andy\"). The chosen name is passed as <code>--assistant-name</code> to\nregister/init.</p>\n<h3>Seed the owner and the primary DM agent</h3>\n<p>The owner identity and the primary agent are created together by\n<code>scripts/init-first-agent.ts</code>. It upserts the user, grants the owner role,\ncreates the agent group + filesystem, wires a DM messaging group, and queues a\nwelcome DM over the running service's CLI socket — <strong>so the service must be\nrunning.</strong> If it isn't, tell the user to start it first.</p>\n<p>Resolve the owner's channel identity and the DM platform id (use the channel's\nown terminology). Then:</p>\n<pre><code>pnpm exec tsx scripts/init-first-agent.ts \\\n  --channel &lt;channel&gt; \\\n  --user-id &lt;channel&gt;:&lt;handle&gt; \\\n  --platform-id &lt;channel&gt;:&lt;dm-id&gt; \\\n  --display-name \"&lt;Owner Name&gt;\" \\\n  --agent-name \"&lt;confirmed assistant name&gt;\" \\\n  [--role owner]      # default: owner\n</code></pre>\n<p>For direct-addressable channels (telegram, whatsapp) the <code>--platform-id</code> is\nusually the same handle as <code>--user-id</code> with the channel prefix. <code>--role</code>\ndefaults to <code>owner</code> (global, cross-channel) — use <code>admin</code> (scoped to the agent\ngroup) or <code>member</code> only if intended.</p>\n<h3>Register the remaining groups</h3>\n<p>For each additional OpenClaw group the user wants to bring over, register a\nmessaging group and wire it to an agent group:</p>\n<pre><code>pnpm exec tsx setup/index.ts --step register -- \\\n  --platform-id \"&lt;v2_platform_id from discovery&gt;\" \\\n  --name \"&lt;group name&gt;\" \\\n  --folder \"&lt;channel&gt;_&lt;name-slug&gt;\" \\\n  --channel \"&lt;channel&gt;\" \\\n  --session-mode \"&lt;shared|agent-shared|per-thread&gt;\" \\\n  [--trigger \"@&lt;assistant name&gt;\"] \\\n  [--no-trigger-required] \\\n  --assistant-name \"&lt;assistant name&gt;\"\n</code></pre>\n<p>Notes:</p>\n<ul>\n<li><code>register</code> namespaces the <code>--platform-id</code> the same way the adapter will at\nruntime, so pass the <code>=&gt;</code> value discovery emitted (or the raw OpenClaw id).</li>\n<li>Reuse a <code>--folder</code> to put a group on an existing agent (shared base/separate\nconversations); use a new <code>--folder</code> for a fully separate agent.</li>\n<li>Engage defaults come from the channel adapter's declaration (most group\nchats default to mention-based engagement; channels without a mention\nsignal default to a name pattern). Pass <code>--trigger</code> to set an explicit\nregex, or <code>--no-trigger-required</code> for respond-to-everything.</li>\n<li>Register groups from channels v2 doesn't support yet too — the messaging\ngroup and wiring persist and activate when that channel is installed.</li>\n</ul>\n<p>Folder naming: <code>&lt;channel&gt;_&lt;name-slug&gt;</code> (e.g. <code>telegram_dev-team</code>). Confirm each\nname and folder with the user.</p>\n<h2>Phase 2: Settings from Config</h2>\n<p>Read the config (<code>&lt;STATE_DIR&gt;/openclaw.json</code> or <code>clawdbot.json</code>) for settings\nthat map to v2 setup.</p>\n<h3>Timezone</h3>\n<p>Check <code>agents.defaults.userTimezone</code>. If it's a valid IANA zone, write it to\n<code>.env</code> as <code>TZ=&lt;timezone&gt;</code>. v2 reads <code>TZ</code> from <code>.env</code> (<code>src/config.ts</code>) and uses\nit for cron/recurrence evaluation, so this matters for scheduled tasks.</p>\n<h3>Container timeout</h3>\n<p>Check <code>agents.defaults.timeoutSeconds</code>. v2's equivalent is <code>CONTAINER_TIMEOUT</code>\n(env var, default 30 min) or per-group <code>ncl groups config update</code>. If the\nOpenClaw value differs notably, note it; the user can set\n<code>CONTAINER_TIMEOUT=&lt;ms&gt;</code> in <code>.env</code>.</p>\n<h3>Access control (sender policies)</h3>\n<p>OpenClaw per-channel <code>allowFrom</code> / <code>dmPolicy</code> / <code>groupPolicy</code> map onto v2's\nmodel, which is <strong>not</strong> a JSON file. Each messaging group has an\n<code>unknown_sender_policy</code>; access is granted via <code>user_roles</code> (owner/admin) and\n<code>agent_group_members</code>. Map:</p>\n<ul>\n<li><p><code>dmPolicy</code>/<code>groupPolicy: \"open\"</code> → leave the default; no extra grants.</p>\n</li>\n<li><p><code>allowFrom</code> / <code>groupAllowFrom</code> lists → for each allowed sender, upsert the\nuser and add them as a member of the relevant agent group via <code>ncl</code>:</p>\n<pre><code>ncl users create --id \"&lt;channel&gt;:&lt;handle&gt;\" --kind &lt;channel&gt; --display-name \"&lt;name&gt;\"\nncl members add --user \"&lt;channel&gt;:&lt;handle&gt;\" --group \"&lt;ag-id&gt;\"\n</code></pre>\n</li>\n<li><p><code>dmPolicy: \"disabled\"</code> → don't wire that chat (or leave it registered but\nunwired).</p>\n</li>\n</ul>\n<p>The messaging groups <code>register</code> / <code>init-first-agent</code> create default their\n<code>unknown_sender_policy</code> to whatever the channel adapter declares for that\ncontext (DM vs group) — <code>strict</code> when the channel has no declaration — so\nunknown senders are gated until you add them (or an admin approves the\nadapter-declared approval card). Pass <code>--unknown-sender-policy</code> to <code>register</code>\nto override. Show the user the OpenClaw allowlist and confirm who to grant\nbefore running the commands.</p>\n<h2>Phase 3: Identity and Memory</h2>\n<p>Fully conversational — read files directly and discuss. <strong>Placement depends on\nthe Phase 1 choice:</strong></p>\n<ul>\n<li><strong>Shared identity:</strong> merge the same core identity/personality into every\nselected group's <code>instructions.prepend.md</code>.</li>\n<li><strong>Fully separate / primary only:</strong> merge identity/personality only into the\ncorresponding group's <code>instructions.prepend.md</code>.</li>\n</ul>\n<p>Never edit a composed <code>CLAUDE.md</code> or <code>AGENTS.md</code>; it is regenerated each spawn.\nPut standing behavior in <code>instructions.prepend.md</code> and facts in <code>memory/</code>.</p>\n<p>Find workspace files at <code>&lt;STATE_DIR&gt;/workspace/</code>. If <code>AGENT_COUNT &gt; 1</code>, also\ncheck <code>&lt;STATE_DIR&gt;/agents/*/workspace/</code> and ask which agent maps to which v2\nagent group.</p>\n<h3>IDENTITY.md / SOUL.md</h3>\n<p>Read them. Distinguish always-loaded vs reference:</p>\n<ul>\n<li><strong>Standing behavior</strong> (core traits, communication style, key rules) → weave\ninto the group's <code>instructions.prepend.md</code>.</li>\n<li><strong>Reference</strong> (backstory, extended guidelines) → a separate durable concept\nin an appropriate folder under <code>groups/&lt;folder&gt;/memory/</code>, linked from that\nfolder's <code>index.md</code> and the root Map.</li>\n</ul>\n<p>Choose each memory folder based on which related information will be easiest to\nfind together; a folder may contain different concept types. Before writing the\nfirst concept into a new folder, create the folder and its <code>index.md</code>. Follow\n<code>memory/system/definition.md</code>, including its YAML frontmatter rules, for every\nnew concept.</p>\n<p>Show proposed edits before applying — this is a thoughtful merge, not a paste.</p>\n<h3>USER.md</h3>\n<p>Create a focused user-context concept in an appropriate memory folder and link\nit through that folder's index and the root Map. Put only facts relevant in\nnearly every conversation (for example name or timezone) into <code>## Core Memory</code>;\nkeep all other details in the linked file.</p>\n<h3>MEMORY.md and daily memory files</h3>\n<p>Show <code>MEMORY.md</code>; keep relevant items in focused concepts under the chosen\nmemory folders, with links through each folder index and the root Map. For\ndaily files (<code>workspace/memory/*.md</code>, count = DAILY_MEMORY_FILES):</p>\n<p>AskUserQuestion: \"You have N daily memory files. How to handle them?\"</p>\n<ol>\n<li><strong>Copy as-is</strong> — agree on a descriptive folder, create it and its <code>index.md</code>,\nthen copy with <code>cp &lt;workspace&gt;/memory/*.md &lt;group_dir&gt;/memory/&lt;chosen-folder&gt;/</code>\nand link the retained files through its index and the root Map.</li>\n<li><strong>Consolidate</strong> — read, extract durable facts, and place them in focused\nlinked memory files.</li>\n<li><strong>Skip.</strong></li>\n</ol>\n<h3>OpenClaw skills</h3>\n<p>If <code>SKILL_COUNT &gt; 0</code>, the SKILL.md format is shared, so skills are portable.\nPresent each (name + description from the front matter) and let the user pick.\nFor each confirmed skill, copy the directory into the container skills tree:</p>\n<pre><code>cp -r &lt;skill_source_dir&gt; container/skills/&lt;skill_name&gt;\n</code></pre>\n<p>A container rebuild is needed afterward — note it for Phase 8.</p>\n<h3>Config-registered plugins (with API keys)</h3>\n<p>If <code>CONFIG_PLUGINS</code> is non-empty, OpenClaw had plugins/skills carrying keys.\nFor each, read the config section and decide together:</p>\n<ul>\n<li><strong>Matching v2 skill</strong> → run that skill; route its credential per Phase 4.</li>\n<li><strong>An MCP server</strong> → install the exact configured package; wire via\n<code>ncl groups config add-mcp-server</code>. Don't guess at packages.</li>\n<li><strong>An API key</strong> → route to the OneCLI vault if container-facing (Phase 4).</li>\n</ul>\n<p>Don't install unknown packages or search for replacements — supply-chain risk.</p>\n<h2>Phase 4: Credentials</h2>\n<p>Two destinations, decided per credential. <strong>Channel tokens → <code>.env</code></strong> (host\nreads them). <strong>Container-facing API credentials → the OneCLI vault</strong> (injected\nper request, never in container env).</p>\n<h3>Channel tokens (telegram, discord, slack)</h3>\n<p>Preview, then write to <code>.env</code>. The script emits only masked values:</p>\n<pre><code>pnpm exec tsx ${CLAUDE_SKILL_DIR}/scripts/extract-channel-credentials.ts \\\n  --state-dir &lt;STATE_DIR&gt; --channel &lt;name&gt;\n</code></pre>\n<p>Parse the status block. <code>DESTINATION: env</code> confirms a host-side token. Show\n<code>CREDENTIAL_MASKED</code> (and <code>CREDENTIAL_MASKED_2</code> for Slack's app token).</p>\n<p>AskUserQuestion:</p>\n<ol>\n<li><strong>Use this credential</strong> — re-run with <code>--write-env .env</code> to save it.</li>\n<li><strong>Enter a new one</strong> — ask in plain text, write to <code>.env</code> yourself.</li>\n<li><strong>Skip this channel.</strong></li>\n</ol>\n<pre><code>pnpm exec tsx ${CLAUDE_SKILL_DIR}/scripts/extract-channel-credentials.ts \\\n  --state-dir &lt;STATE_DIR&gt; --channel &lt;name&gt; --write-env .env\n</code></pre>\n<p>Check <code>WRITTEN_TO</code> / <code>WRITTEN_COUNT</code>. Slack writes both <code>SLACK_BOT_TOKEN</code> and\n<code>SLACK_APP_TOKEN</code> in one run.</p>\n<p><strong>If <code>HAS_CREDENTIAL=false</code> but a credential is expected:</strong> the config shape may\nbe unrecognized, or it uses a <code>file</code>/<code>exec</code> SecretRef (<code>CREDENTIAL_SOURCE</code>\nends in <code>_ref</code> with a NOTE) that can't be auto-extracted. Read the channel\nsection of the config directly and ask the user to confirm or paste the value.</p>\n<p><strong>WhatsApp:</strong> authenticates via QR/pairing code — there's no token. Don't copy\nBaileys auth state (stale encryption sessions break decryption).\nRe-authenticate during <code>/setup</code> via <code>/add-whatsapp</code>. The extraction script\nreports <code>DESTINATION: none</code> for it.</p>\n<h3>Anthropic and other container-facing credentials → OneCLI vault</h3>\n<p>Find the agent's model credentials in OpenClaw. Check, in order:</p>\n<ol>\n<li><code>&lt;STATE_DIR&gt;/auth-profiles.json</code> (and\n<code>&lt;STATE_DIR&gt;/agents/&lt;id&gt;/agent/auth-profiles.json</code>) — a <code>profiles</code> map keyed\n<code>provider:identifier</code>. For an <code>anthropic</code> provider profile the value depends\non <code>type</code>: <code>api_key</code> → <code>key</code>, <code>token</code> → <code>token</code>, <code>oauth</code> → <code>access</code>.</li>\n<li><code>&lt;STATE_DIR&gt;/.env</code> — <code>ANTHROPIC_API_KEY</code> or <code>CLAUDE_CODE_OAUTH_TOKEN</code>.</li>\n<li>Config <code>models.providers</code> — Anthropic provider <code>apiKey</code>.</li>\n</ol>\n<p>These are container-facing, so they go to the OneCLI vault. Do <strong>not</strong> write\nthem to <code>.env</code> or thread them into a container. Register each in the vault:</p>\n<pre><code>onecli secrets create --name Anthropic --type anthropic \\\n  --value &lt;key-or-token&gt; --host-pattern api.anthropic.com\n</code></pre>\n<p>For other container-facing keys discovered in plugins (e.g. OpenAI):</p>\n<pre><code>onecli secrets create --name OpenAI --type api_key \\\n  --value &lt;key&gt; --host-pattern api.openai.com\n</code></pre>\n<p>Run the command on the user's behalf so the value never lands in the chat\ntranscript; confirm with <code>onecli secrets list</code>.</p>\n<p><strong>Caveats:</strong> <code>keyRef</code>/<code>tokenRef</code> with <code>source:\"exec\"</code> or <code>source:\"file\"</code> can't\nbe auto-extracted — ask the user to paste it. For an <code>oauth</code> profile with a\npast expiry, warn that the token may need refreshing; the user can run\n<code>claude setup-token</code> and register the fresh token.</p>\n<p>If OneCLI isn't installed yet, defer this: tell the user that during <code>/setup</code>\n(or <code>/init-onecli</code>) they'll register the Anthropic credential, and note the\ndiscovered profile in <code>migration-state.md</code> so it isn't lost.</p>\n<blockquote>\n<p>There is no supported <code>.env</code>-credentials opt-out anymore: the session spec's\nadmission rules refuse credential values in container env on every lane, by\ndesign (the retired <code>/use-native-credential-proxy</code> skill would be denied at\nevery spawn). Credentials go through the OneCLI vault; custom Anthropic\nendpoints use <code>ANTHROPIC_BASE_URL</code> plus the placeholder-token pattern from\nsetup, with the gateway rewriting the header on the wire.</p>\n</blockquote>\n<h2>Phase 5: Scheduled Tasks</h2>\n<p>Read <code>&lt;STATE_DIR&gt;/cron/jobs.json</code>. If absent or empty, skip.</p>\n<p>If jobs exist, read <code>${CLAUDE_SKILL_DIR}/MIGRATE_CRONS.md</code> for the v2 task\nmodel, the <code>mapCronToRecurrence</code> transform, the full field mapping, and how\ntasks are created (the agent's <code>schedule_task</code> MCP tool, since tasks live in a\nper-session <code>inbound.db</code> the host owns). Follow it for each enabled job.</p>\n<h2>Phase 6: MCP, Webhooks, Other Config</h2>\n<p>Read the relevant config sections directly. Conversational.</p>\n<h3>MCP servers</h3>\n<p>If <code>MCP_SERVERS</code> is non-empty, v2 supports per-agent-group MCP servers via the\ncontainer config. Read each server's <code>command</code>/<code>args</code>/<code>env</code>/<code>url</code> from\n<code>mcp.servers</code>. For each one the user wants:</p>\n<pre><code>ncl groups config add-mcp-server --id &lt;agent-group-id&gt; \\\n  --name &lt;server-name&gt; --command &lt;cmd&gt; \\\n  [--args '&lt;json-array&gt;'] [--env '&lt;json-object&gt;']\n</code></pre>\n<p>stdio servers must be runnable inside the container (Node/npx-based work;\ncustom binaries need a Dockerfile addition). Secrets referenced by a server's\n<code>env</code> should go to the OneCLI vault (Phase 4), not be inlined. The config\nchange takes effect on restart: <code>ncl groups restart --id &lt;agent-group-id&gt;</code>\n(add <code>--rebuild</code> only if a custom binary was added to the Dockerfile).</p>\n<h3>Webhooks</h3>\n<p>OpenClaw <code>cron.webhook</code> / <code>failureDestination</code> / channel webhooks don't map to\na v2 primitive. For a notification webhook, fold it into a scheduled task's\nprompt or a pre-agent <code>script</code> that <code>curl</code>s the endpoint. Discuss the use case.</p>\n<h3>Other config (mention and move on)</h3>\n<ul>\n<li><strong>Exec approvals / command allowlist</strong> → v2 uses container isolation; the\nagent runs sandboxed.</li>\n<li><strong>Human delay / TTS / compaction / model config</strong> → not v2 task/group fields\n(per-group model is in the container config).</li>\n</ul>\n<h2>Phase 7: Welcome and First Run</h2>\n<p><code>init-first-agent</code> (Phase 1) already queued a welcome DM for the primary owner\nagent. If the service was up, the owner should have received it. For groups\nregistered via <code>setup --step register</code>, the wiring also queues a <code>/welcome</code>\nonboarding message on first wiring.</p>\n<p>Tell the user which agents are live now and which await channel installation\n(unsupported channels registered for the future).</p>\n<h2>Phase 8: Validate and Summarize</h2>\n<h3>Run the shipped test</h3>\n<p>Copy the transform module and its test into the project so vitest runs them\nagainst the composed install, then build and test:</p>\n<pre><code>cp ${CLAUDE_SKILL_DIR}/scripts/transform.ts        scripts/openclaw-transform.ts\ncp ${CLAUDE_SKILL_DIR}/scripts/transform.test.ts   scripts/openclaw-transform.test.ts\n# Point the copied test at the copied module name:\nsed -i.bak \"s#from './transform.js'#from './openclaw-transform.js'#\" scripts/openclaw-transform.test.ts &amp;&amp; rm -f scripts/openclaw-transform.test.ts.bak\n\npnpm run build\npnpm exec vitest run scripts/openclaw-transform.test.ts\n</code></pre>\n<p>The test guards the skill's two v2 integration assumptions: credential routing\n(container-facing → vault, channel tokens → <code>.env</code>) and the cron → v2\nrecurrence mapping. It imports the real <code>cron-parser</code> (the same parser the host\nrecurrence sweep uses), so a missing/renamed dependency turns it red. <code>build</code>\ntypechecks the transform module against the project.</p>\n<p>These copied files are the only files the skill installs into the project tree;\n<code>REMOVE.md</code> deletes them.</p>\n<h3>If a container rebuild is needed</h3>\n<p>If OpenClaw skills were copied or MCP servers added: <code>./container/build.sh</code>,\nthen restart the service.</p>\n<h3>Summary</h3>\n<p>Print what was migrated:</p>\n<ul>\n<li>Owner + primary agent → <code>users</code> / <code>user_roles</code> / agent group + welcome DM</li>\n<li>Additional groups → messaging groups + wiring (folders + session modes)</li>\n<li>Timezone → <code>.env TZ</code>; container timeout → noted</li>\n<li>Access grants → members/roles for OpenClaw allowlist senders</li>\n<li>Identity/personality → per-group <code>instructions.prepend.md</code> + linked memory concepts</li>\n<li>User context / memories → Core Memory only for universal facts; otherwise\nlinked concepts in content-based folders under <code>memory/</code></li>\n<li>OpenClaw skills → <code>container/skills/</code></li>\n<li>Channel tokens → <code>.env</code> (list channels)</li>\n<li>Container-facing credentials → OneCLI vault (list)</li>\n<li>Scheduled tasks → mapped and scheduled via the agent (or noted for first run)</li>\n<li>MCP servers → wired into agent group container configs</li>\n</ul>\n<p>Noted for later: channel installs during <code>/setup</code>; container rebuild if needed;\ntasks deferred until a session exists.</p>\n<p>Not applicable: unsupported channels (registered for the future); OpenClaw-only\nfeatures (exec approvals, human delay, TTS, model/thinking config).</p>\n<p>Remind: \"Run <code>/setup</code> next to finish your NanoClaw install. Channel tokens are\nin <code>.env</code>; container-facing credentials are in the OneCLI vault. Select the\nchannels we configured when setup asks.\"</p>\n<p>Then delete <code>migration-state.md</code> (or offer to keep it as a record), and remove\nthe copied transform files if you don't want them lingering (see <code>REMOVE.md</code>).</p>\n<h2>Troubleshooting</h2>\n<ul>\n<li><strong>Config parse error:</strong> the JSON5 parser may not handle unusual syntax. Read\nthe file directly and work with it manually.</li>\n<li><strong>Credential not found:</strong> likely a <code>file</code>/<code>exec</code> SecretRef — ask the user to\npaste the value.</li>\n<li><strong><code>init-first-agent</code> can't reach the CLI socket:</strong> the service isn't running.\nStart it, then re-run.</li>\n<li><strong>Multi-agent complexity:</strong> do the primary/default agent first; add others as\nseparate agent groups later.</li>\n</ul>\n","files":[{"path":"MIGRATE_CRONS.md","sizeBytes":6212,"isText":true},{"path":"REMOVE.md","sizeBytes":2935,"isText":true},{"path":"scripts/discover-openclaw.ts","sizeBytes":23701,"isText":true},{"path":"scripts/extract-channel-credentials.ts","sizeBytes":10097,"isText":true},{"path":"scripts/transform.test.ts","sizeBytes":7512,"isText":true},{"path":"scripts/transform.ts","sizeBytes":11543,"isText":true},{"path":"SKILL.md","sizeBytes":24030,"isText":true}],"reviewScore":null,"reviewSummary":null,"trust":{"provenance":"trusted-source-unreviewed","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow.","bodySource":null},"bodyLocked":false,"purchaseUrl":null,"sourceUrl":null,"report":{"provenance":"trusted-source-unreviewed","screen":{"ran":true,"outcome":"notes-only","suspicious":0,"notes":43,"hiddenCharacters":false},"virusScan":{"engine":"clamav","status":"clean","scannedAt":"2026-08-24T05:39:32.73145Z","sha256":"1492B851CAC011287B958DAC03C95CC67496ED21A30EB66CBA3F9164CCD36220","sizeBytes":30984},"review":null,"source":{"repositoryUrl":"https://github.com/nanocoai/nanoclaw","path":".claude/skills/migrate-from-openclaw","license":"MIT","commit":"143db6c907c652773a536c7c9e96269fdad0a4a4","subtreeSha":"B6896E461E9C1FFA6751D931D676C296C61CCA324AE93089C1370DF8AD5E00AC","lastSyncedAt":"2026-09-24T06:48:46.168681Z"},"reviewedAt":"2026-08-24T05:46:43.101999Z","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow."},"install":[{"target":"skills-cli","command":"npx skills add https://github.com/nanocoai/nanoclaw/tree/main/.claude/skills/migrate-from-openclaw"},{"target":"claude-code","command":"claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install nanocoai-nanoclaw@llmmart"},{"target":"git","command":"git clone https://github.com/nanocoai/nanoclaw.git"}]}