A therapist's website. WordPress, Elementor, bilingual French-English, hosted on Combell. My mission: a redesign. But before touching a single pixel, I need a faithful working copy on my machine. Here is the full workflow, executed in about an hour, almost entirely driven by Claude Code.
The starting point
A WordPress 6.9 site built with Elementor, translated into English with TranslatePress, running on Combell shared hosting. No documentation, no Git, no history: just access to the hosting panel. And 1.3 GB reported on the server side, more than half of which, I don't know it yet, is dead weight.
My rule on every takeover: production stays untouched. You observe, you copy, you work on the clone. The only thing I install on the server is a key.
The SSH key: everything hinges on it
The question that kicked off the session: if I create an SSH key, do I still need the password? Answer: no. And that answer changes the whole workflow.
An SSH key is a pair of files. The private key stays on my machine and never leaves it. The public key is made to be shared: you drop it at the host, and the server now knows how to recognize my machine. No shared secret, nothing to remember, nothing to type.
# Generate a key pair (if you don't already have one)
ssh-keygen -t ed25519
# Print the public key to copy
cat ~/.ssh/id_ed25519.pub
The ed25519 format fits in 68 characters where an RSA key takes around 400, with security equivalent to RSA-3072. On Combell, installing it is a simple copy-paste into the "SSH keys" form of the panel. I did it myself, in the browser: the account password never traveled anywhere. Not in a terminal, not in a command, not through the AI.
First connection test, with a flag I recommend:
ssh -o PasswordAuthentication=no myaccount@ssh0xx.webhosting.be 'echo CONNECTED'
PasswordAuthentication=no forces a clean failure if the key doesn't work, instead of silently falling back to a password prompt. One trap along the way: the ssh.mydomain.be alias advertised by Combell didn't resolve yet; the server's real hostname, on the other hand, always works.
- No more password - nothing to remember, nothing to type, nothing to store in plain text in a script.
- Nothing goes through the AI - the public key is not a secret, and the private key never leaves the machine.
- One tunnel for everything - ssh, scp, rsync, and later the deployment to staging.
- Automation becomes possible - the AI chains dozens of remote commands without intervention.
The local environment first
Before pulling anything down, the machine must be ready to serve the site: Apache vhost over HTTP and HTTPS, a local certificate signed by mkcert (the .test domain joined a multi-domain certificate that now holds 104 of them), a MySQL database in utf8mb4, an entry in /etc/hosts. Twenty minutes of mechanics, scripted from a template reused from project to project.
One guardrail I keep everywhere: the AI never restarts my services. It prepares the configuration, validates it with httpd -t, then hands me the command. I'm the one typing sudo.
Remote inventory, then repatriation
With the key in place, the inventory happens remotely, read-only. And it pays off immediately: WP-CLI is available on the server, and out of the reported 1.3 GB, 724 MB are just old plugin backups that have no business being in a clone.
The database first: a WP-CLI dump straight on the server, compressed before transfer:
ssh myaccount@ssh0xx.webhosting.be 'cd www && wp db export /tmp/dump.sql && gzip -f /tmp/dump.sql'
scp myaccount@ssh0xx.webhosting.be:/tmp/dump.sql.gz ./_import/
145 MB of SQL become 16 MB of transfer. On an Elementor site, the dump is dominated by JSON that compresses beautifully: nine times fewer bytes on the wire.
Then the files, with rsync and a list of exclusions: backups, caches, logs. Final result: 23,952 files and 438 MB instead of 1.3 GB. But not on the first try, and the failures are worth telling:
- macOS no longer ships the real rsync - the system provides openrsync, which rejects modern options like
--info=progress2. Instant failure. - A pipe disguises the failure -
rsync ... | tailreturns tail's exit code, never rsync's. The transfer was declared "successful" while zero files had been copied. Since then:set -o pipefailand a log written to a file. - Absolute paths are mandatory for background jobs - a relative
./launched from the wrong directory copies the site... elsewhere. Emergency stop, check, clean restart.
Adapting the clone without breaking anything
A WordPress clone doesn't boot on its own. The list of adaptations is short, but every one of them is non-negotiable:
- wp-config.php rebuilt from scratch - the production one contains the live server's MySQL credentials. It gets archived outside the served site, never reused.
- Fresh salts - reusing the production security keys would allow forging, from the clone, a valid session cookie on the live site. Regenerated through the wordpress.org API.
- Domain search-replace - 19,420 replacements of the production URL with the local one, including 2,459 inside serialized PHP.
wp search-replacerecomputes string lengths; a raw sed would have destroyed every Elementor layout. - Production plugins neutralized - the LiteSpeed cache (white pages without the server module), Wordfence (timeouts locally), and above all two-factor authentication: its TOTP codes are tied to the devices registered in production. Without deactivation, the clone's admin is locked out forever.
One scare to finish: three files with shady names at the root, as if left behind by an intruder. Line-by-line analysis, looking for the classic backdoor markers: nothing. Two manual copies of legitimate WordPress files and a defensive rename performed by Wordfence. A strange name is not a compromise: you verify before you conclude.
Verifying before claiming victory
First page load: 1,162 bytes served, and the content of another local project. Two failures stacked on top of each other. Apache was running in memory with a configuration older than the new vhost: one restart, and the request finally lands in the right place. Then a maintenance index.html forgotten since 2023 was silently masking all of WordPress: in production, the .htaccess bypassed it; locally, it didn't. Moved away, case closed.
Only then, the battery of checks: homepage at 200 and 183 KB, login, contact page and English version at 200, certificate accepted without a warning, zero residual occurrences of the production domain in the HTML. Last check, a visual one: a screenshot driven through Chrome's DevTools protocol. Logo, menu, slider, language switcher: the clone is faithful.
Plugging the AI into WordPress: two MCP plugins
The clone runs. What's left is giving the AI clean access to the inside of WordPress, not just to the server. That's the job of two plugins, activated locally only:
- MCP Adapter - the official bridge from WordPress.org's "AI Building Blocks" initiative: it exposes the WordPress Abilities API through the MCP protocol, the one spoken by Claude and other agents.
- EMCP Tools - the Elementor layer: more than a hundred tools to read and manipulate widgets, templates, popups, global settings and media, with sensitive write operations disabled by default.
The two channels complement each other: the SSH key talks to the server (files, database, WP-CLI), the MCP plugins talk to WordPress itself (content, Elementor design). Authentication goes through a dedicated Application Password, not the login password. And these plugins stay on the clone: none of this ships to production.
The numbers
| Metric | Value |
|---|---|
| Session duration | about 1 hour |
| Remote site reported | 1.3 GB, including 724 MB of excluded backups |
| Files repatriated | 23,952 files, 438 MB |
| SQL dump | 145 MB, 16 MB once compressed |
| Database imported | 106 tables, 28 pages, 18 posts, 134 media files |
| URL replacements | 19,420, including 2,459 in serialized PHP |
| SSH password used | 0 times |
What about the development server?
It's the next step of the same workflow, and the SSH key will serve again as is. I've already battle-tested the mechanics on another WordPress deployed to shared staging: database export with wp search-replace --export (serialization handled out of the box), a tar.gz archive of the site excluding wp-config.php and the two MCP plugins, rsync transfer, then an import forcing --default-character-set=utf8mb4. Without that last flag, accented characters get double-encoded along the way and every "N°" turns into hieroglyphs. Finally: blog_public=0 to block indexing of the staging site, and a purge of the CSS files generated by Elementor.
The principle stays the same in both directions: the live site is never a working environment. Local to build, staging to show, production to publish.
The limits
- A clone contains real data - forms, accounts, historical entries. That dump is not shared, not versioned, not sent to anyone. GDPR doesn't stop at production.
- The clone is not a staging site - it proves the site runs on my machine, not that a major update will survive production. The Elementor 3 to 4 migration, for instance, will wait for a real staging.
- The AI executes, it doesn't decide - every sensitive action stayed behind a human validation: the sudo, the service restarts, the choice of what to repatriate.
The takeaway
- The SSH key is the first move, not a detail - ten minutes of setup, and the entire rest of the workflow becomes scriptable.
- WP-CLI on the server changes the scale - dump, search-replace, plugin management: everything happens in commands, no web interface.
- Failures are data - openrsync, the lying pipe, the ghost index.html: every documented trap saves an hour on the next project.
- Nothing magical here - a key, a tunnel, standard tools that are twenty years old. What's new is that an AI chains them tirelessly and documents everything along the way.
The resources
The three key tools of the workflow, all open source and free.
mkcert WP-CLI MCP Adapter