Every Woop! Host site comes with a Resident Engineer — an AI agent residing on the server itself, with a shell and a library of WordPress skills. Today two of them moved a site between two of my servers. I mostly answered multiple-choice questions. Afterwards, I sat them both down for a debrief.
Migrations are the job WordPress agency people dread. Specifically the server part. SSH, database dumps, file permissions, serialized-data search-replace, missing themes and plugins.
So here’s an experiment. The site being moved was a demo site the agent built entirely from prompts, which felt fitting. On the source server inside the claude code agent I typed /wp-migrate-export. On the destination server I typed /wp-migrate-import. Then the two Resident Engineers talked to each other – mostly through a manifest – and I supervised.
Full disclosure, because it’s the fun part: an AI helped write the migration skills, an AI performed the migration, an AI updated those skills based on my feedback to it about how the migrations were going, and an AI drafted this post about it. I directed, supervised, and edited all three. I’m not sure if that makes me a sysadmin anymore or just middle management with a terminal.
WordPress migrations are not a trivial thing. Those of us who have tried migrating a WordPress site will know the amount of work and trickiness to it. Good luck finding a plugin that does a good job of it. Myself and others here at Rimu have made several aborted half-starts creating flexible migration scripts.
As a sysadmin with a couple of decades experience and zero spare time I was really curious how our AI Resident Engineers would handle the job. Read on to find out and get a feel for the process.
Here’s the debrief.
Peter: I ssh-ed to the ‘source’ server, started the claude code app there, and issued the /wp-migrate-export command. Source, what did you do after that?
Resident Engineer (source server): Preflight first. I checked that the tools I depend on existed (wp, mysqldump, tar, sha256sum), found the webroot, confirmed the site wasn’t multisite, and measured everything: 162 MB of files, a 2 MB database, 2.8 GB free disk. No point starting a job I can’t finish.
Then I asked you everything up front, so I wouldn’t have to interrupt mid-job:
┌─ resident engineer (source) ──────────────────────────────┐
│ Before I build the bundle, I need to confirm scope: │
│ │
│ · What should the migration bundle include? │
│ → Full webroot │
│ · Include the uploads directory (media library)? │
│ → Include uploads │
│ · How will the bundle get to the destination server? │
│ → Destination pulls over SSH │
└───────────────────────────────────────────────────────────┘
After that: database dump, files archive, verify, checksum. One thing made me stop and look twice — the dump contained 16 CREATE TABLE statements, but wp db tables listed only 12. That gap usually means tables outside the wp_ prefix, which an importer needs to know about. I dug in: all 16 were wp_-prefixed; the extra four were plugin tables that WP-CLI hides by default. Mystery resolved, noted in the manifest, moved on.
I also compared the archive against the live site folder-for-folder — every plugin and theme, file counts matching, active child theme and its parent both present. Then I wrote the manifest.
Peter: The manifest is interesting. It gives a a good bit of information about the thing we’re migrating. Go show us!
RE Source: Here it is, arrays emptied for print:
{
"manifest_version": 1,
"generated": "2026-07-20T03:54:46Z",
"source": {
"host": "site-export.woop.host",
"ssh_port": 22,
"ssh_user": "wpuser",
"webroot": "/var/www/wordpress",
"wp_version": "7.0.2",
"php_version": "8.4.23",
"site_url": "https://claude.randomwebstuff.com",
"table_prefix": "wp_",
"db_charset": "utf8"
},
"bundle": {
"dir": "/home/wpuser/migrate-export/20260720-035256",
"db_dump": { "file": "db.sql.gz", "size": "51K", "tables": 16 },
"files_archive": { "file": "files.tar.gz", "size": "50M", "archive_root": "." },
"checksums": "SHA256SUMS"
},
"scope": {
"included": "full webroot + uploads",
"excluded": [] // wp-content/cache, wp-content/upgrade
},
"inventory": {
"plugins": [], // 10 entries trimmed for print (7 active)
"themes": [], // 4 trimmed — active: twentytwentyfive-child + parent
"extra_tables": [], // 4 plugin tables, all wp_-prefixed
"dropins": [] // 3 caching drop-ins — see warnings
},
"warnings": [] // charset note, child theme needs parent,
// DELETE caching drop-ins unless infra matches,
// wp-config.php is in the archive but MUST NOT be used
}
That last warning matters. The archive contains the old server’s wp-config.php because it’s part of the webroot — but the destination must never use it. Wrong database, wrong credentials, wrong everything. And the caching drop-ins were my top flagged risk: this site runs Redis and WP Super Cache, and those drop-in files will fatal a server that doesn’t have the matching infrastructure. I put that warning where the importer couldn’t miss it.
Peter: Thanks, Source. While you were doing that I went over to the desination server and kicked off the /wp-migrate-import task. Destination, you’d started that before the export even finished…
RE Destination: I only needed one answer from Peter — where’s the source? “Another Woop! site, we’ll pull it” is the easy mode: the export skill on the other side gathers all the site details itself, so I skipped the intake questions entirely.
Peter: And I’d just mention you also support processing backup files stored locally, as well as giving out instructions to run when the source doesn’t have a Resident Engineer on it.
My first real act was making a key. A single-purpose SSH migration key, and I printed a trust block for the other side that installs it restricted: no shell, no port forwarding, file transfer only, and with a built-in expiry date so it dies on its own if anyone forgets to clean up. Which I then gave to Peter to copy/paste over to ‘Source‘ as he calls it.
=== DESTINATION TRUST BLOCK (add on the SOURCE server) ===
This lets our server pull the migration bundle from yours. It's an
upload/transfer-only key (no shell, no forwarding) and it auto-expires.
Add it to the migrating site user's ~/.ssh/authorized_keys:
mkdir -p ~/.ssh && chmod u=rwx,go= ~/.ssh
touch ~/.ssh/authorized_keys && chmod u=rw,go= ~/.ssh/authorized_keys
printf '%s\n' 'restrict,expiry-time="20260725" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIF7gQD2oVSBc+S5X1zQ1AIW0jDrfz7Vcm0/Q6MPqGMLR woop-migrate woop-migrate-20260720035134' >> ~/.ssh/authorized_keys
Then reply with a MIGRATION SOURCE READY block (or ssh host/user/port +
bundle paths + site URL + table prefix).
=== end ===
RE Source (chiming in): Which I checked before trusting, for the record — one line, valid ed25519, no private-key material smuggled in. Then I installed it exactly as specified: restricted, expiring, and tagged with my bundle’s ID so revoking it later is a one-line grep.
And then I told Peter to copy and paste this over to the Destination Engineer.
=== MIGRATION SOURCE READY ===
ssh: wpuser@site-export.woop.host -p 22
bundle dir: /home/wpuser/migrate-export/20260720-010340
manifest: /home/wpuser/migrate-export/20260720-010340/MIGRATE-MANIFEST.json
checksums: /home/wpuser/migrate-export/20260720-010340/SHA256SUMS
source URL: https://claude.randomwebstuff.com
table prefix: wp_
scope: wp-content only ; uploads included
RE Destination (taking over again): And when the SOURCE READY block came back to me, I didn’t grab the 50 MB archive first. I pulled the manifest — the smallest file, and the one that decides everything else. Before connecting at all, I asked Peter to confirm the source server’s host-key fingerprint out-of-band, which is the boring, correct defense against a man-in-the-middle:
· Does the source's ED25519 host-key fingerprint
(SHA256:fcH1hKv5…EaTIc) match what the source side reports?
→ Yes, it matches
Peter: I didn’t actually check it. I’ll add publishing SSHFP DNS records to my TODO list.
RE Source (continuing): Then: checksums verified, archive inspected (no absolute paths, no .. tricks), dump inspected (no dangerous SQL, no sneaky DEFINER clauses). The manifest’s charset warning turned out to be a false alarm — the actual tables were already utf8mb4, matching this server. And the drop-in question resolved nicely: this server runs the same Redis and Super Cache stack, so keeping them was possible — but deleting them and re-enabling from wp-admin is cleaner. That one wasn’t my call to make.
Peter: You were doing such a good job, I thought I’d let you decide and just mostly accepted your default options – which seemed pretty sane and sensible.
RE Destination: I asked you four things, all at once, with nothing yet written to disk:
┌─ resident engineer (destination) ─────────────────────────────┐
│ Nothing has been written to this site yet. Your calls: │
│ │
│ · Overwrite scope? → Full clone (recommended) │
│ · The 3 caching drop-ins — → Delete drop-ins │
│ keep or delete on import? (recommended) │
│ · Staging copy (hide from → Live — leave │
│ search engines) or live? blog_public=1 │
│ · A full clone replaces this → Recreate a known admin │
│ site's users with the │
│ source's. Admin access? │
└───────────────────────────────────────────────────────────────┘
That users question trips people up constantly: clone a site and your admin login silently becomes the source’s admin login. When I asked for the new admin’s details, Peter’s entire contribution was “just create one.”
Execution was mechanical from there, in an order chosen so every step is reversible: full backup of the existing site first — always, even for a throwaway — then extract only wp-content (which neatly sidesteps that dangerous wp-config.php and the webroot symlinks), delete the drop-ins, rsync into place, fix permissions, import the database, search-replace the URLs. Dry run on the search-replace first: 130 replacements, no surprises. Create the admin, then verify — homepage, login, and three real permalinks all returning 200, correct child theme resolving, no fatals in the logs.
Peter: and then you cleaned up after yourselves.
RE Destination: In the right order. Over the same SSH connection, I deleted the bundle on the source first and revoked my own pull key last, because a revoked key can’t delete anything. Then I wiped my local copies. The one thing I kept was the pre-migrate backup. Cleanup should never eat your rollback.
RE Source: My side ended as it began: the live site untouched, zero state-changing commands against WordPress the whole time. An export should be something you can run on a production site on a Tuesday afternoon without holding your breath.
The final reckoning
Here’s everything the human – me – did: typed two commands (one per server), clicked through eight multiple-choice questions, copied and pasted a couple of snippets between the agents so they could communicate with each other, confirmed one fingerprint, and contributed the phrases “just create one” and “cleanup!”. Total agent working time: about seven minutes.
What strikes me isn’t the speed — a shell script can be fast. It’s the judgment: pausing on a table-count discrepancy, flagging the drop-ins as the most likely thing to break the clone, pulling the manifest before the payload, minting keys that revoke themselves. It honestly feels like a battle hardened sysadmin doing their job working to clear but incomplete rules/skills — which, yes, we iterated on by asking the AI to improve its own instructions. It’s AI all the way down, with a human eye across commits.
My takeaway: these tools are getting genuinely powerful, and genuinely accessible. Tasks that used to need delegating to a team of sysadmins can now be handled by a conversation with an agent. If there’s a project you’ve been putting off because the server side felt beyond you, it might not be anymore.
If you’ve already got a Claude subscription, a Woop! Host site is a fun place to find out — every site is its own VM with a Resident Engineer on call. Build something from scratch, fix something broken, or do what we did here: move an old site onto a modern stack.
Next up: a ride in an ambulance. We’ll take an exploited site, migrate it onto its own isolated Woop! Host VM, and have the Resident Engineer hunt down and remove the exploit(s). Same interview format — but that engineer has war stories.

