Closes #141923 ## What Problem This Solves Fixes an issue where `openclaw backup create` publishes nothing when included state contains an absolute symbolic link to an external file or directory, including a dangling target. This reworks @adele-with-a-b's reproduction and contribution under the maintainer's explicit recovery policy: preserve links and their targets, report external targets durably, and recreate the links during restore. ## Why This Change Was Made One archive-entry policy preserves the original link target and classifies whether it names content outside the state directory, even when another declared asset includes the target. The existing traversal records external links. The manifest is sealed after that traversal and includes `externalSymbolicLinks`; no link target is followed to copy content. The decision `how a symbolic link is recorded and whether its target is external` is made by exactly one mechanism at `src/infra/backup-archive-path-policy.ts:54`. Restore extracts payload files and creates every link parent directory before creating the verified links. Archive path normalization preserves filename whitespace. Verification rejects entries beneath a symbolic link and checks that external-link records match the archive. Writer and reader share the existing 1 MiB manifest limit; oversized metadata fails before publication. This intentionally reverses the absolute/escaping-target rejection from #123672 (`8c7e86a2ea`) and the owned-absolute rewrite from #136343 (`0aa9ae9f3e3`). @steipete @vsumner: the maintainer-directed recovery policy now preserves the original target text. ## User Impact Backups complete with ordinary absolute, relative, and dangling links. The manifest, create summary, JSON output, and restore warnings identify outside-state targets. Creation never copies content through a link; a separately declared config, credentials, or workspace asset can still include that content. Absolute links still name their original locations after restore; review those paths before activating a relocated backup. No database schema, migration, config key, CLI flag, or manifest version changes. No installation/update behavior change is claimed. Existing managed-root exclusions and SQLite snapshot/sanitization owners remain in place. Compatibility findings: v2026.9.4 rejects preserved absolute or escaping targets. Such archives require the updated reader. Older portable relative archive links remain supported, including Windows links between drives. Absolute owned links are no longer rewritten for relocation, as required by literal target preservation. The manifest now appears after the payload and always includes an external-link report, which can be empty. Readers retain the older portable-relative archive contract when that report is absent. Old cross-asset links are still reported as outside-state during verification and restore. ## Evidence - Current head `712293765774e11ac6b01ca0ce3fb839d98e5e97` adds one required test diagnostic label to the unchanged production implementation. CI caught the omitted second argument to `expectDefined`; both affected command cases and formatting pass after correction. Fresh source review confirms reuse of the production and independent proof below with their original tested identities. Corrected-input CI must establish type validity. - Production proof on `a634bd60fa5e1dbb361c8ee5d3c77c07d418ed27`: rebuilt production CLI create/verify/restore and text create all exit 0; four separately included outside-state links produce four manifest/output records and preserve literal restored targets. The actual v2026.9.4 source build restores its ordinary archive with exact bytes, and rejects its absolute-link archive before creating the target. - Census correction: the prior candidate preserved four links to separately included outside-state config, credentials, and workspace targets, but reported zero. Production CLI proof captures that failure. The corrected shared policy uses the state boundary and preserves literal targets. - The writer seals the canonical state directory used by inventory. A command regression reproduced a verification mismatch when an enclosing workspace covers state accessed through an alias; the canonical manifest fact resolves it. - Pinned main `0a991fdc64`: production CLI `backup create --verify --json` exits 1 on the external file link; no archive published. - Committed candidate `23ace9e1304471b162bad241fc32aaf18e9c7080`, with subsequent CI and containment corrections recorded below: production CLI creation and verification succeed; four link entries preserve targets; three external targets appear in the manifest and output. - Previously reviewed candidate `dde971d1d42d24560160d62d50140665b150e441`: fresh independent CLI proof passes on its exact build. Seven links preserve targets, including trailing-space names and both dangling forms; all four external links are reported. Internal links read restored bytes after source mutation; external content stays absent. Both ordinary and space-suffixed crafted descendants are refused before target creation, with the complete external directory unchanged. - v2026.9.4-created archive restores on candidate. The actual v2026.9.4 source build restores the candidate no-link archive (exit 0), but rejects the candidate external-link archive before extraction (exit 1: symbolic link target must be relative). - No-link control on final head: payload paths, types, modes, targets, and bytes match pinned base after normalizing run identity. Manifest differences are its final position and an explicit empty `externalSymbolicLinks` report. The actual v2026.9.4 source reader restores this new ordinary archive byte-for-byte. - Hardlink control: 1,000 hardlinks stall on the same entry at the unchanged 300000ms watchdog on pinned main and candidate. This is the adjacent #145429 failure; no hardlink fix is claimed. - Third-party SQLite: baseline and candidate creation pass; candidate restore preserves the original vendor table row. - Manifest-limit stress: 1,300 links with long targets exceed the existing 1 MiB manifest bound. Creation exits 1 with an explicit size error and publishes no archive. - Focused create/verify/restore owner tests pass. Four stale command tar mocks were repaired; the command and atomic-publication sibling run then passed all 23 tests. Scoped production/test lint, formatting, line limits, assertion checks, and unused exports pass. All four structural checks also pass on the exact merge tree. The unused-export scan needed a frozen workspace install to load its browser-test dependency; the original setup failure is retained. Full typechecks and suites remain CI-owned. - First-head CI exposed stream/filter type mismatches; the correction uses the producer’s `AsyncIterable<Buffer>` contract and the verifier’s recorded link paths. Local typechecks remain disabled; exact-head CI owns that proof. - Live review found a space-suffixed link ancestor bypass. Production CLI reproduction confirmed an external write on the prior candidate. The corrected normalizer retains filename spaces, and restore finishes all parent directories before exclusive link creation. Restore/verify/atomic tests pass: 76, with one existing platform skip. - Supplemental census-correction proof on `00f727bb6da9e1f8e09f0a19f87ef4e40576806a`: a fresh source-blind validator exercised 22 CLI invocations. Nine outside-state first hops were reported across all outputs; 11 literal links and separately declared asset bytes survived. Aliased state inside an enclosing workspace, old portable cross-asset archives, malformed report refusals, both link-descendant refusals, and the previous-stable artifact all passed. - Execution proof is macOS arm64 only. Linux and native Windows restoration remain unproven. ## Consumers | Reader or writer | Disposition | | --- | --- | | `register.backup.ts` → `backupCreateCommand` → `createBackupArchive` | Same CLI path and options; writer retains links and emits report. | | `backup-archive-path-policy.ts` | One link policy replaces the remapper plus rejecting guard. Entry-path containment remains separate from link-target text. | | `backup-create-stream.ts` | Streams payload once and appends the finalized manifest before gzip completes. | | `backup-verify-manifest.ts` | Reads additive external records; owns the shared manifest byte limit. | | `backup-verify.ts` listing/verification | Existing tar listing is the metadata census; uses the same link policy, matches report, and rejects link ancestors. | | `backup-restore.ts` | Consumes verified link records after ordinary extraction and all-parent-directory preparation; recreates exact targets with exclusive creation. | | `migrate/apply.ts` pre-migration backup | Uses the same verified backup; report survives inside its archive even though create text is suppressed. | | `backup-health.ts`, `backup-run-records.ts`, status/Doctor | Existing success/failure receipt contract remains unchanged; no separate archive parser. | | `scripts/e2e/lib/external-package-transition.sh` | Calls the registered CLI and retains JSON; it does not parse the manifest or link records. This external-install harness is not an updater proof. | | `src/infra/backup-create.test.ts`, `src/commands/backup-verify.test.ts`, `src/commands/backup-restore.test.ts` | Direct typed owner-test readers. The test ledger replaces target rejection and rewriting with literal-target, external-report, and restored-filesystem assertions through the registered command entry points; containment and older-reader cases remain covered. | | `src/cli/program/register.backup.product-path.test.ts:13` | Launches the actual create CLI and verifies completion when SQLite capture outlives the audit lease. The existing `backup-cli-registration` lane collected its passing case in job 103526025671, log line 11081. | | `src/cli/help-exit.process.test.ts:451` | Launches actual create CLI cases for an absolute configured-config link and excluded workspace. `test/vitest/vitest.cli-process-paths.mjs:10` assigns it to the separate process group. Both cases passed in job 103526024850, log lines 1593–1594; required receipt `backup-cli-process` binds this consumer and output. | | `backup-capture-privacy.test.ts`, `backup-create.sqlite-hardlinks.test.ts`, `backup-config-capture.test.ts`, `backup-create.legacy-audit-lease.test.ts` | Typed-reference census found existing test consumers. Calls and inputs remain supported; core owner and hardlink/SQLite endpoint evidence cover the touched archive behavior. Their full CI test lanes remain required. | | `backup.test-support.ts`, `backup.test.ts`, `backup.atomic.test.ts`, `backup.create-verify.test.ts` | The mock stream contract was updated; finished-archive assertions replace temporary-manifest callback inspection. All 23 command/atomic sibling tests pass. | | `src/commands/backup-shared.ts` | Unchanged shared payload encoder and plan fields remain supported. Caller canonicalization supplies inventory.stateDir; raw plan.stateDir continues serving existing config/lease planning. The new manifest/report boundary consumes the canonical inventory path without changing the planner contract. | | `src/commands/backup-resource-inventory.ts` | Unchanged stateDir declaration remains the canonical inventory boundary. New writer metadata reuses this authoritative fact rather than creating a competing state owner. | | `src/infra/backup-sqlite-snapshot.ts` | Unchanged inventory.stateDir consumers classify snapshot paths, locate gateway coordination, walk the canonical state tree and set isolated snapshot environment. Reporting reuse does not alter these snapshot/lease side effects or database ownership. | | SQLite backup list/verify/restore, Git backups, Fleet and skill collection backups | Separate artifact formats; unchanged. There is no native whole-archive list command. | | `docs/cli/backup.md` | Documents literal link targets, durable reporting, extraction order, and the previous-reader compatibility limit. | | `docs/install/backups.md` | Removes the unqualified portable-archive promise; states that absolute links retain original locations, including separately backed-up config/credentials, and need review before relocated activation. Links the detailed CLI caveat. | | `docs/install/migrating.md` | Machine-move instructions now require reviewing original absolute targets, including separately backed-up config/credentials, before activation at another location. Links the detailed CLI caveat. | | `docs/install/digitalocean.md`, `docs/install/oracle.md`, `docs/install/raspberry-pi.md` | Deployment backup instructions replace unqualified portability claims with archive transfer/staging and review of original absolute targets before activation, including separately backed-up config/credentials. Each links the detailed CLI caveat. | | `docs/install/updating/rollback-and-recovery.md:195` | Unchanged explicit pre-update backup command and manifest/source-path guidance. It does not promise automatic link relocation or exercise the installed-software updater. | | `docs/cli/reset.md:40`, `docs/cli/uninstall.md:34` | Unchanged commands recommend an archive before removal and use supported create options; no archive parser or relocation rule. | | `docs/cli/migrate.md:78` | Unchanged description of the verified pre-migration backup uses the same create command; no separate format or link policy. | | `docs/plugins/manifest/surfaces.md:174` | Unchanged `--only-config` example and plugin resource inclusion contract; link preservation does not change manifest resource discovery. | | `docs/cli/doctor/sqlite-maintenance.md:32,70` | Backup-advice consumers are outside the explicit Doctor-advice contract; maintenance wording remains unchanged. | | `docs/releases/2026.7.1.md` | Historical release statements are records of shipped behavior, not current operator instructions to rewrite. | The process receipt supplements the retained prior-head evidence for run 34683341770, attempt 1, event `pull_request`: `checks-node-compact-small-12` / `agentic-cli-process-hosted-6` passed eight files with one skipped and 53 tests with five skipped (job 103526024850, log lines 1629–1630). The corrected receipt set includes required lane `backup-cli-process`; registration proof already includes the product-path process case. These receipts do not claim execution of a later revision. The preceding receipt plan bound head `a634bd60fa5e1dbb361c8ee5d3c77c07d418ed27`, merge `665241f7662257d8339b71b79fd28dab939aa268`, and run 34685894510 attempt 1. The writer lane is `checks-node-compact-large-36` (103532821764); command consumers moved to `checks-node-compact-small-4` (103532820738); registration uses `checks-node-compact-small-9` (103532822348); the separate required `backup-cli-process` lane uses `checks-node-compact-small-12` (103532821731). That run exposed the missing test-helper argument in `check-test-types-core-1`; it cannot supply a passing type receipt. Run 34686603366 on the corrected head must supply fresh completed output for the same 15 required structural and affected lanes. Prior receipt identities remain historical, not current-head success. Unchanged-helper reference expansion is explicitly dispositioned here. The queried `writeJson` and `realpathSync` contracts are unchanged in `src/infra/json-files.ts`, `src/infra/json-files.test.ts`, `src/infra/install-package-dir.ts`, `src/infra/npm-managed-root.ts`, `src/skills/loading/workspace-skill-sync.runtime.ts`, `src/skills/lifecycle/clawhub-store.ts`, `src/skills/lifecycle/source-install.ts`, `src/agents/cli-runner/bundle-mcp-runtime.ts`, `src/cli/update-cli/update-command-post-core.ts`, `scripts/lib/package-dist-inventory.ts`, `src/plugin-sdk/json-store.ts`, `src/wizard/setup.migration-promotion.ts`, `src/gateway/test-helpers.config-runtime.ts`, and `src/gateway/test-helpers.server.ts`. Dependency declarations `@openclaw/fs-safe/dist/json.d.ts` and `@types/node/fs.d.ts` are also unchanged. These are helper-reference results, not affected archive consumers; no SDK or updater change or shipped-consumer proof is requested for them. CLI startup/config-preflight readers that only recognize unchanged command spelling likewise do not consume the archive contract. No additional affected native app, channel, plugin runtime, SDK export, locale, or persistence consumer was found in the inspected source/lexical closure. Removed `assertArchiveSymbolicLinkTarget` had only native create and verify consumers; both use `recordArchiveSymbolicLink`. The private absolute remapper is deleted. No removed policy remains reachable. Typed census: pinned-base and final merge-tree runs use explicit `tsconfig.core.json`, `test/tsconfig/tsconfig.core.test.commands.json`, and `test/tsconfig/tsconfig.core.test.infra.json` projects. Selected declarations and import bindings are covered in the loaded projects; the raw parsed JSON property has an explicitly recorded dynamic-query gap. All returned reader locations have retained dispositions, including unchanged `writeJson` and `realpathSync` helper users. Unloaded projects and sources remain explicit coverage gaps; no whole-repository typed coverage is claimed. The retained prior-head 86-position batch ran on CI merge `f7c0b811779ffbbca93b2e3047807d8683a1bfba`; all three projects loaded and analysis exited 0. The lexical census found no remaining reader of the removed policy, no manual rows, and no generic exemptions. The shared filename normalizer also serves manifest asset membership, SQLite snapshot-root discovery, duplicate/portable-collision checks, and older rootless hardlink lookup. Each now retains significant whitespace; declaration/import and individual reader dispositions are retained with the typed results. The ordinary and trailing-space hardlink tests remain green. <details> <summary>Typed coverage gaps</summary> Unloaded projects (136): `extensions/{a2a, acpx, admin-http-rpc, alibaba, amazon-bedrock-mantle, amazon-bedrock, anthropic-vertex, anthropic, arcee, azure-speech, baseten, brave, browser, buzz, byteplus, cerebras, chutes, clawrouter, clickclack, cloudflare-ai-gateway, codex, cohere, comfy, copilot-proxy, copilot, deepgram, deepinfra, deepseek, diagnostics-otel, diagnostics-prometheus, diffs, discord, duckduckgo, elevenlabs, exa, fal, featherless, feishu, firecrawl, fireworks, geolocation, github-copilot, gmi, google-meet, google, googlechat, gradium, groq, huggingface, image-generation-core, imessage, inworld, irc, kilocode, kimi-coding, line, litellm, llm-task, lobster, longcat, matrix, mattermost, memory-core, memory-lancedb, memory-wiki, meta, microsoft-foundry, microsoft, minimax, mistral, moonshot, msteams, mxc, nextcloud-talk, nostr, novita, nvidia, ollama, openai, opencode-go, opencode, openrouter, openshell, parallel, perplexity, pixverse, qa-channel, qa-lab, qianfan, qwen, raft, reef, runway, searxng, sglang, signal, slack, sms, stepfun, synology-chat, synthetic, tavily, teams-meetings, telegram, tencent, tlon, together, tokenjuice, twitch, venice, vercel-ai-gateway, vllm, voice-call, volcengine, vydra, webhooks, whatsapp, xai, xiaomi, zai, zalo, zalouser, zoom-meetings}/tsconfig.json`; `extensions/tsconfig.json`; `extensions/tsconfig.package-boundary.base.json`; `extensions/tsconfig.package-boundary.paths.json`; `packages/ai/tsconfig.json`; `packages/llm-core/tsconfig.json`; `packages/model-catalog-core/tsconfig.json`; `packages/plugin-sdk/tsconfig.json`; `test/tsconfig/tsconfig.extensions.test.json`; `tsconfig.extensions.json`; `tsconfig.extensions.projects.json`; `tsconfig.json`; `tsconfig.scripts.json`; `tsconfig.ui.json`. These projects were not selected and remain explicit coverage gaps. </details> The retained prior-head merge census has 441 distinct symbol/location records, each with a retained semantic disposition. Of 258 project-position queries, 211 resolve and 47 are outside one selected project but covered by another; no position is wholly unresolved. All 13 files from that prior head had empty candidate-to-merge diffs. The lexical PR inventory uses final rebase base `0ffc3f0fb1`; the original failure baseline remains pinned separately. Supplemental correction BASE census: 48 positions on the prior tested merge, whose affected owner files match `dde971d1`; all 144 project queries resolve, with 253 distinct reader dispositions across 11 files. No projects failed. This supplements the immutable original baseline census. The new merge-tree query and its exact coverage are recorded below. Previous-candidate typed census on merge `665241f7662257d8339b71b79fd28dab939aa268`, tree `0c10bb805dde42acfbe16cce50587b595f120690`, for head `a634bd60fa5e1dbb361c8ee5d3c77c07d418ed27`: 146 freshly selected positions cover the original 86-position batch, the 48-position correction BASE batch, and new state/report-presence declarations and aliases. All three selected projects loaded and analysis exited 0 with no project failures. The 3,080 returned references reduce to 758 distinct symbol/location records across 23 files; every record has a retained source-based disposition. Of 438 project-position queries, 382 resolve. Another 53 are outside one selected project and resolve in a named sibling project. Three query instances are the same raw JSON position, `src/commands/backup-verify-manifest.ts:187:16`: `parsed.externalSymbolicLinks` has no declared member symbol because `parsed` is narrowed to `Record<string, unknown>`. This is an explicit dynamic-field coverage gap. The parser function, `parsed` local, `BackupManifest.externalSymbolicLinks` declaration, and returned shorthand at :187:61 resolve. Source review traces the presence check at :150/:187: an absent field stays absent; a present array is validated and copied. The verifier consumes that distinction at `src/commands/backup-verify.ts:672,684`; command tests distinguish absent, empty, and populated reports at `src/commands/backup-verify.test.ts:1542,1555,1574`. No unresolved export edge was returned. The 136 unloaded projects remain explicit coverage gaps. All 66,307 source-outside-project records remain named by project without truncation in the coverage artifact; this includes overlapping exclusions and is not a distinct-file count. Native code, docs, configuration, process-launch strings and dynamic wire keys remain manual-census surfaces. No whole-repository typed coverage is claimed. All 17 PR files and all 23 returned-reader files have empty candidate-to-merge diffs. Six dependency manifests/project configs and 95 direct relative imports also match. The merge contains 353 other changed files, retained in the dependency record; no whole-tree or complete transitive runtime equivalence is claimed. The original failure BASE and supplemental prior-candidate BASE remain separate evidence. Fresh typed analysis completed on merge `c044a4f1d0ed93856127e2d72c2e69e362a37c39`, tree `d0111aa9cfce690f1f98e825012d0e6f21d890cb`, for head `712293765774e11ac6b01ca0ce3fb839d98e5e97`. Its 146 checked positions retain the original 86-position scope, the 48-position supplemental BASE scope, and new canonical-state/report-presence declarations and aliases. All three selected projects loaded; exit 0, no project failures. The 3,080 raw references reduce to 758 distinct symbol/location records across 23 files. Every returned record has a fresh source-based disposition with its full query/project origins and enclosing context retained. Of 438 project-position queries, 382 resolve. Another 53 miss within one selected project but resolve in a named sibling project. Three instances represent one persistent dynamic-field gap at `src/commands/backup-verify-manifest.ts:187:16`: `parsed.externalSymbolicLinks` is a raw wire member on `Record<string, unknown>`, so it has no declared member symbol. The parser function, `parsed` local, typed manifest property and returned shorthand at :187:61 resolve. Manual source review confirms the checks at :150/:187 preserve absent versus present reports; verifier lines :672/:684 consume that distinction. Verifier tests at :1542/:1555/:1574 cover empty, missing and populated reports. No unresolved export edge was returned; all 133 export records retain dispositions. The 136 unloaded projects remain named coverage gaps. All 66,315 source-outside-project records are retained without truncation, including overlapping project exclusions; this is not a distinct-file count. The selected scope is `tsconfig.core.json`, `test/tsconfig/tsconfig.core.test.commands.json` and `test/tsconfig/tsconfig.core.test.infra.json`. Native code, docs, configuration, process-launch strings and dynamic wire keys remain manual-census surfaces. This is not whole-repository typed coverage. All 17 task files, all 23 returned-reader files, six dependency/configuration files and 95 direct relative-import edges (54 unique target files) match between candidate and merge. The merge also contains 391 other file changes, listed in the dependency record. The comparison establishes no whole-tree or complete transitive-runtime equivalence. The original pinned failure BASE and supplemental BASE evidence remain unchanged and separately identified. The only candidate change since the previous census adds the required `expectDefined` context argument in the restored workspace-file assertion. Fresh positions were checked against the new merge, and later returned test locations were reviewed with their one-line shift. This correction adds no production contract. Previous failed type evidence remains failed; corrected-head CI is separate required evidence. Current receipt plan: head `712293765774e11ac6b01ca0ce3fb839d98e5e97`, merge `c044a4f1d0ed93856127e2d72c2e69e362a37c39`, run 34686603366 attempt 1. Writer job 103534715513, command job 103534714257, registration job 103534716527, process job 103534715637, and corrected infra-type job 103534712831 are explicitly required. All 15 structural and affected lanes need successful completed output; the plan is not a success receipt. Pipeline endpoint: restored filesystem entries, their exact `readlink` values, readable internal content, and untouched external content. ## Invalidation No persistent cache is added. The external-link list belongs to one archive attempt and is cleared before each existing retry. The final manifest derives from observed write entries. Verification derives new link records from the selected archive for each invocation; restore consumes those verified records. ## Contention No new shared lock, queue, or database transaction is held. Existing SQLite/audit snapshot capture completes before tar traversal. Compression, temporary output, and the link report belong to one archive operation. ## Tests New and rewritten tests name `backupCreateCommand`, `backupVerifyCommand`, and `backupRestoreCommand`. Coverage includes separately included outside-state config/credentials/workspace targets (absolute and relative), an aliased state directory covered by a workspace, absent versus empty legacy reports, file/directory/dangling/internal/external links, exact manifest records, old Windows cross-drive links, managed-skill non-promotion, literal backslashes, and refusal of files or links beneath an archived symlink. Existing hardlink and SQLite coverage remains. Test deletion ledger: obsolete target-rejection and owned-target-rewrite expectations are replaced by literal-target and restored-file assertions in the same owner tests. No whole test file is removed. ## Context Related #145429 remains a draft hardlink-stall repair. This diff does not change its link-cache or traversal policy, but both PRs edit the tar-options owner in `src/infra/backup-create.ts`: #145429 replaces the link-cache helper and adds serial hardlink options. There is no semantic landing order. Whichever lands second must preserve both the hardlink options and this manifest/link path, then prove the combined writer. #144552, #107433, and #40786 are adjacent issues outside this repair. Co-authored-by: Ayaan Zaidi <hi@obviy.us>
7.5 KiB
| summary | read_when | title | |||
|---|---|---|---|---|---|
| Migration hub: cross-system imports, machine-to-machine moves, and plugin upgrades |
|
Migration guide |
OpenClaw supports three migration paths: importing from another agent system, moving an existing install to a new machine, and upgrading a plugin in place.
Import from another agent system
Bundled migration providers bring instructions, MCP servers, skills, model config, and (opt-in) API keys into OpenClaw. Plans are previewed before any change and secrets are redacted in reports. Standalone openclaw migrate is backed by a verified backup; fresh onboarding imports instead stage and verify local artifacts before publishing them with configuration committed before any irreversible external activation.
The CLI entry point is openclaw migrate. Onboarding can also offer migration when it detects a known source (openclaw onboard --flow import).
Move OpenClaw to a new machine
Copy the state directory (~/.openclaw/ by default) and your workspace to preserve:
- Config —
openclaw.jsonand all gateway settings. - Auth — shared and per-agent SQLite auth stores (API keys plus OAuth), plus any channel or provider state under
credentials/. - Sessions — conversation history and agent state.
- Channel state — WhatsApp login, Telegram session, and similar.
- Workspace files —
MEMORY.md,USER.md, skills, and prompts.
Migration steps
On the **old** machine, stop the Gateway, then create and verify a backup archive:```bash
openclaw gateway stop
mkdir -p ~/Backups/openclaw
openclaw backup create --output ~/Backups/openclaw --verify
```
Stop the Gateway before taking a machine-move snapshot. A raw copy of a
changing SQLite database can capture mismatched database and WAL files;
quiescing the Gateway also keeps the rest of the state tree stable. If you
use multiple profiles, run the command once with each profile selected.
[Install](/install) the CLI (and Node if needed) on the new machine. It is fine if onboarding creates a fresh `~/.openclaw/` — you overwrite it next.
Transfer the generated `.tar.gz` archive via `scp`, an external drive, or
another protected channel. On the new machine, restore it to a fresh
staging directory:
```bash
openclaw backup restore <archive.tar.gz> --target ~/openclaw-restored
```
Restore never activates in place. With the Gateway stopped, use the
restored `manifest.json` mapping to move the state and workspace assets to
their recorded destinations, or point `OPENCLAW_STATE_DIR` at the restored
state asset. Confirm ownership matches the user that will run the Gateway.
Absolute symbolic links keep their original target locations, including
links to separately backed-up config or credentials. Before activating
state on another machine or at another path, review these links and make
sure their targets are correct for the new location. See the
[backup symbolic-link caveat](/cli/backup#what-gets-backed-up).
<Warning>
Restoring older channel state can desynchronize ratcheting credentials such
as WhatsApp. Approvals and delivery/dedupe state also roll back, and plugin
`node_modules` trees must be reinstalled. See [Restore a full archive](/install/backups#restore-a-full-archive).
</Warning>
On the new machine, run [Doctor](/gateway/doctor) to apply config migrations and repair services:
```bash
openclaw doctor
openclaw gateway restart
openclaw status
```
If Telegram or Discord uses the default env fallback (TELEGRAM_BOT_TOKEN or DISCORD_BOT_TOKEN), verify the migrated state-dir .env contains those keys without printing the secret values:
awk -F= '/^(TELEGRAM_BOT_TOKEN|DISCORD_BOT_TOKEN)=/ { print $1 "=present" }' ~/.openclaw/.env
openclaw doctor also warns when an enabled default Telegram or Discord account has no configured token and the matching env variable is unavailable to the doctor process.
Common pitfalls
If the old gateway used `--profile` or `OPENCLAW_STATE_DIR` and the new one does not, channels will appear logged out and sessions will be empty. Launch the gateway with the **same** profile or state-dir you migrated, then rerun `openclaw doctor`. The config file alone is not enough. Shared model auth lives in `state/openclaw.sqlite`, agent-local profiles live in `agents//agent/openclaw-agent.sqlite`, and channel and provider state lives under `credentials/`. Always migrate the **entire** state directory using the backup and restore flow above. If you copied as root or switched users, the gateway may fail to read credentials. Ensure the state directory and workspace are owned by the user running the gateway. If your UI points at a **remote** gateway, the remote host owns sessions and workspace. Migrate the gateway host itself, not your local laptop. See [FAQ](/help/faq#where-things-live-on-disk). The state directory contains auth profiles, channel credentials, and other provider state. Store backups encrypted, avoid insecure transfer channels, and rotate keys if you suspect exposure.Verification checklist
On the new machine, confirm:
openclaw statusshows the gateway running.- Channels are still connected (no re-pairing needed).
- The dashboard opens and shows existing sessions.
- Workspace files (memory, configs) are present.
Upgrade a plugin in place
In-place plugin upgrades preserve the same plugin id and config keys but may move on-disk state into the current layout. Plugin-specific upgrade guides live alongside their channels:
- Matrix migration: encrypted-state recovery limits, automatic snapshot behavior, and manual recovery commands.
Related
openclaw migrate: CLI reference for cross-system imports.- Install overview: all installation methods.
- Doctor: post-migration health check.
- Updating: updating an existing install in place, plus rollback strategy.
- Uninstall: removing OpenClaw cleanly.
openclaw backup— create the archive this migration restores