Most WordPress admin is clicking. Log in, wait for the dashboard, tick twelve plugins, click Update, wait, check nothing broke. Five minutes of attention for thirty seconds of actual work, and that is on one site.
WP-CLI is the command line version of the same thing. It is a PHP application that boots WordPress without a browser and gives you commands for nearly everything the admin does, plus a few things it cannot do at all. Updating every plugin on a site is one command. Resetting a locked-out client’s password takes about four seconds. Changing a domain across a database, which is genuinely dangerous by hand, becomes a safe, previewable operation.
This is not a reference; the official documentation already exists. It is the subset I use week in week out on client sites, in roughly the order I learned to trust it, including the flags that stop you doing damage.
Before you start
You need SSH access to the server and the site’s document root. Most decent UK hosts give you SSH on any plan above the cheapest; shared hosting at the budget end often does not, and that alone is a reason to move.
Take a database export before anything that writes. WP-CLI does exactly what you tell it, immediately, with no confirmation dialogue and no undo. That is the whole point of it, and it is also the risk. The first command in this article is the backup command for that reason.
Step 1: Check whether the host already has it
Connect over SSH, change into the site’s root (the folder containing wp-config.php) and run:
wp --info
If it is installed you get the WP-CLI version, the PHP binary in use and the PHP version. If you get “command not found”, check the usual host-specific paths before assuming it is missing, then install it if you need to:
# Download, check it runs, make it executable and put it on PATH.
curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
php wp-cli.phar --info
chmod +x wp-cli.phar
sudo mv wp-cli.phar /usr/local/bin/wp
Pay attention to the PHP version in that output. The CLI often uses a different PHP binary from the web server, and a site on PHP 8.2 in the browser can be running WP-CLI under PHP 7.4 from a system package. That mismatch causes errors that make no sense until you spot it. If they differ, tell WP-CLI which binary to use with the WP_CLI_PHP environment variable.
Confirm it can see the site:
wp core version --extra
That prints the WordPress version, the database revision, the TinyMCE version and the package language. If it errors about the database, you are in the wrong folder or wp-config.php cannot connect.
Step 2: Take a backup you can actually restore
# Export the whole database to a dated file.
wp db export "backup-$(date +%Y%m%d-%H%M%S).sql"
To restore, wp db import backup-20260115-101500.sql. That is it. Two commands, and they are the reason everything else in this article is safe to try.
Worth knowing:
wp db size --tables --format=table # find the table that has quietly grown to 4GB
wp db optimize # runs OPTIMIZE TABLE across the database
wp db check # checks for corruption
Do not leave .sql files in the web root where anyone can download them. Move them above the document root, or delete them once they are safely off the server. A database export contains every password hash on the site.
Step 3: Updates without the dashboard
This is the daily win. Check what needs doing:
wp core check-update
wp plugin list --update=available --fields=name,version,update_version
wp theme list --update=available
Then update. --dry-run on plugin updates tells you what would happen without doing it:
wp plugin update --all --dry-run
wp plugin update --all
wp theme update --all
wp core update
wp core update-db # run after a core update, especially on multisite
Update one thing at a time when a plugin is known to be temperamental:
wp plugin update woocommerce
wp plugin update woocommerce --version=8.5.2 # roll back to a specific version
That version flag is a genuinely useful recovery tool. An update breaks checkout, you roll the plugin back to the previous version in one command, and the site works again while you investigate properly.
Installing and managing plugins is just as quick:
wp plugin install wordpress-seo --activate
wp plugin deactivate --all # for debugging a white screen
wp plugin activate --all
wp plugin deactivate hello akismet # several at once
wp plugin delete hello
wp plugin list --status=inactive # find plugins nobody is using
Deactivating everything and reactivating one at a time is the standard way to find which plugin broke a site, and it takes a minute over SSH instead of twenty in a slow dashboard. If the site is already fatally broken, add --skip-plugins --skip-themes so WP-CLI boots WordPress without loading the thing that is crashing:
wp --skip-plugins --skip-themes plugin deactivate the-broken-one
That single flag has rescued more sites for me than any plugin. It is the first thing I try when a site has gone down.
Two more that are worth running on any site you have inherited:
wp core verify-checksums # compares core files against wordpress.org
wp plugin verify-checksums --all
Anything reported as modified is either a bad manual edit or an injected file. On a clean install both commands should report success and nothing else.
Step 4: Users and the fastest lockout fix there is
A client rings, they cannot log in, the reset email is not arriving because the host’s mail is broken. Over SSH:
wp user list --role=administrator --fields=ID,user_login,user_email
wp user update 1 --user_pass='a-long-unique-passphrase'
Done. No database editing, no MD5 hash generator, no risk of pasting into the wrong column. WordPress hashes it properly with its own function, which is the important part.
If you have no SSH access, the manual route through phpMyAdmin still works and is written up here: changing a WordPress password directly in the database. WP-CLI is the better option whenever you have the choice, because there is no chance of leaving a weak MD5 hash behind.
Creating and checking users:
wp user create ash ash@example.com --role=administrator --user_pass='...'
wp user list --field=user_login # every username on the site
wp user reset-password 1 --show-password # generates a strong one for you
wp user delete 12 --reassign=1 # move their content to user 1
Note that command shows a password in your terminal history. Clear it, or use wp user reset-password 1 on its own, which emails the user instead.
Step 5: search-replace, and why you must not do it in SQL
This is the command that justifies WP-CLI on its own.
When you move a site between domains, the old URL is scattered through the database. The obvious fix is a SQL UPDATE ... REPLACE() query. Do that and you will break the site, possibly in ways you do not notice for weeks.
The reason is serialisation. WordPress stores arrays and objects (widget settings, theme options, page builder layouts, plugin configuration) as serialised PHP strings, and those strings record the byte length of every value inside them. A serialised string looks like s:21:"https://oldsite.co.uk", where 21 is the length. Replace the URL with a shorter or longer one in raw SQL and the recorded length no longer matches. PHP then refuses to unserialise the whole option, and it fails silently. Your widgets vanish, your Elementor layouts come back blank, your theme options reset to defaults.
WP-CLI unserialises each value, replaces inside it, and reserialises with correct lengths:
# ALWAYS run the dry run first and read what it says. Give it the same
# flags as the real run, or the preview is not of the run you will do.
wp search-replace 'https://oldsite.co.uk' 'https://newsite.co.uk' \
--dry-run \
--skip-columns=guid \
--report-changed-only
# Happy with the report? Run it for real.
wp search-replace 'https://oldsite.co.uk' 'https://newsite.co.uk' \
--skip-columns=guid \
--report-changed-only
Three flags that matter:
--dry-runprints a table of how many rows in each table would change, and touches nothing. There is no reason ever to skip it.--skip-columns=guidleaves theguidcolumn alone. It looks like a URL but it is a permanent unique identifier for feed readers, and changing it makes subscribers see every old post as new.--all-tables-with-prefixincludes tables WordPress does not own but that share the prefix, which is where plugins like WooCommerce and form builders keep data. Without it, those tables are skipped and you get a half-migrated site.
Add --precise if a page builder stores data in a way the fast path mangles; it is slower but uses PHP’s own serialisation handling throughout. On multisite, add --network.
Always search-replace the protocol and host together (https://oldsite.co.uk, not just oldsite.co.uk) so you do not accidentally rewrite email addresses or unrelated text containing the domain.
Step 6: Caches, transients, rewrites and cron
The small stuff that fixes odd behaviour:
wp cache flush # object cache (Redis, Memcached); no-op without one
wp transient delete --expired # clear expired transients
wp transient delete --all # clear the lot, including plugin update checks
wp rewrite flush # equivalent to re-saving Settings > Permalinks
wp rewrite list --format=table # see what rules actually exist
wp rewrite flush is the command for the classic “single posts 404 but the homepage is fine” problem. That is nearly always a custom post type whose rewrite rules were never saved, and this is the one-line fix.
Cron is where WordPress hides its slow problems:
wp cron event list # what is scheduled and when it is next due
wp cron event run --due-now # run everything that is overdue
wp cron event run wp_scheduled_delete # run one specific event
wp cron test # check WP-Cron can reach itself over HTTP
wp cron event list on a neglected site often shows hundreds of overdue events, which is a sign that WP-Cron is not firing (nothing is visiting the site, or DISABLE_WP_CRON is set with no real cron replacing it). The standard fix is a server cron entry every five minutes:
*/5 * * * * cd /var/www/example.co.uk && /usr/local/bin/wp cron event run --due-now --quiet
Then set define( 'DISABLE_WP_CRON', true ); in wp-config.php so it stops running on page loads. On a busy site that is a small but real speed improvement, because visitors stop paying for other people’s scheduled tasks.
Step 7: Wrap it in a script that updates a site safely
Once the commands are familiar, the sequence is always the same: back up, update, verify. Put it in a shell script so you never do it out of order at nine on a Friday evening.
#!/usr/bin/env bash
# update-site.sh /var/www/example.co.uk https://example.co.uk
set -euo pipefail # stop on the first error, and treat unset variables as errors
SITE_PATH="${1:?Usage: update-site.sh <path> <url>}"
SITE_URL="${2:?Usage: update-site.sh <path> <url>}"
BACKUP_DIR="$HOME/wp-backups"
STAMP="$(date +%Y%m%d-%H%M%S)"
mkdir -p "$BACKUP_DIR"
cd "$SITE_PATH"
echo "==> Backing up the database"
wp db export "$BACKUP_DIR/db-$STAMP.sql" --quiet
echo "==> Backing up wp-content"
tar -czf "$BACKUP_DIR/content-$STAMP.tar.gz" wp-content
echo "==> Updating"
wp core update --quiet
wp core update-db --quiet
wp plugin update --all --quiet
wp theme update --all --quiet
echo "==> Verifying"
wp core verify-checksums # fails loudly if a core file has been altered
# Check the front end really returns 200, not a fatal error page.
STATUS="$(curl -s -o /dev/null -w '%{http_code}' -L "$SITE_URL")"
if [ "$STATUS" != "200" ]; then
echo "!! Site returned HTTP $STATUS after updating."
echo "!! Restore with: wp db import $BACKUP_DIR/db-$STAMP.sql"
exit 1
fi
echo "==> Done. Site returned 200. Backup: $BACKUP_DIR/db-$STAMP.sql"
The verification step is the part people leave out, and it is the only part that turns an unattended update into something you can trust. Without it you have automated the updating and not the noticing.
Run it on staging first, always. And be clear about what it does not do: it does not check whether checkout still works, whether the layout survived, or whether a plugin update quietly changed a setting. Automated updates plus an HTTP status check is a floor, not a substitute for someone looking. That human check is most of what a maintenance plan actually is.
Step 8: Running it over SSH, and the —allow-root question
You do not have to log in to the server first. WP-CLI can run over SSH from your own machine:
wp --ssh=deploy@example.co.uk/var/www/example.co.uk plugin list
Better, define aliases once in a wp-cli.yml file in your project:
@production:
ssh: deploy@example.co.uk/var/www/example.co.uk
@staging:
ssh: deploy@staging.example.co.uk/var/www/staging
Then wp @staging plugin update --all, or wp @production db export from your laptop. Aliases stop the mistake where you think you are on staging and you are not.
On --allow-root: WP-CLI refuses to run as root by default, and that refusal is protecting you. Any file it creates while running as root (an upgrade folder, a cache file, a log) is owned by root, and the web server user then cannot write to it. The symptoms turn up later as failed updates and unwritable uploads, and nobody connects them to the command you ran.
The right fix is to run as the user that owns the files:
sudo -u www-data -i -- wp --path=/var/www/example.co.uk plugin update --all
Check the owner first with ls -l in the site root; it might be www-data, nginx, apache or a per-site user depending on the host. Reach for --allow-root only in a container where everything already runs as root and there is no other user to be, and never as a habit on a shared server.
How to check it worked
After any WP-CLI session, the check is the same as after any other change: load the front page and load the admin, both in a private window so you are not seeing a cached or logged-in version.
For updates specifically, wp core check-update and wp plugin list --update=available should come back empty. For a search-replace, load a few inner pages and check for mixed content warnings in the browser console, and open a page built with a page builder, because that is where broken serialisation shows up first. For cron, wp cron event list should show a normal spread of future dates and not a wall of overdue ones.
When it does not work
”Error: This does not seem to be a WordPress installation”
You are in the wrong directory, or the site root is a level down. Either cd into the folder containing wp-config.php or pass --path=/full/path/to/wordpress.
”Error establishing a database connection”
WP-CLI reads the same wp-config.php as the site. If the browser works and the CLI does not, it is usually the database host: some setups use a socket that is available to the web server but not from your shell, or the credentials are set through an environment variable that is not present in your SSH session.
A PHP fatal error stops every command
A broken plugin or theme is crashing before WP-CLI can act. Use --skip-plugins --skip-themes to get in, then deactivate the culprit.
”Allowed memory size exhausted” on a big command
Give the CLI more memory just for that run: php -d memory_limit=512M "$(which wp)" search-replace ....
search-replace reports zero changes
The search string does not match exactly. Check for a trailing slash, http against https, or www against non-www. Search for something short and unmistakable with --dry-run to prove the string exists first.
The command ran but the site looks unchanged
Something is caching. Flush the object cache, clear any page cache plugin, and clear the CDN if there is one. WP-CLI changes the database; it does not know about the cache in front of it.
Common questions
Is WP-CLI safe to run on a live site?
The read commands are completely safe. The write commands do exactly what you ask with no confirmation, which is why the export in Step 2 comes first. Nothing here is more dangerous than the equivalent dashboard action; it is just faster, and speed is unforgiving.
Does it work on managed hosts?
Most managed WordPress hosts ship it, and some restrict specific commands (wp db operations are sometimes blocked, or core updates are managed for you). wp --info connects, and you find out the rest by trying.
Can I use it on Windows?
Yes, most easily through WSL. You can also run the .phar directly with PHP on Windows, though paths and quoting get fiddly.
Do I need to be a developer to use this?
No. Copying and running the commands here needs care, not PHP knowledge. Writing your own commands is a different matter, and that is where WP-CLI gets genuinely powerful: any plugin can register its own, which is worth knowing if you are having something custom built.
What about multisite?
Nearly everything takes --url=https://sub.example.com to target one site, or --network where the operation applies across the whole install. Always read which one a command expects; getting it wrong on a search-replace is expensive.
Why it is worth the afternoon
The commands here cover the large majority of what I do to a WordPress site that is not writing code: updating it, backing it up, moving it between domains, resetting access, clearing state and checking nothing is quietly broken. Each is one line, each is repeatable, and each can go in a script.
If you learn three of them, make it wp db export, wp search-replace --dry-run and wp plugin update --all. The first makes everything else recoverable, the second turns the most dangerous routine job on a WordPress site into a safe one, and the third gives back most of the time you currently spend watching a progress bar in the dashboard.
If your host does not offer SSH, that is worth changing on its own. A great deal of what makes a WordPress site quick to fix, quick to move and quick to maintain depends on being able to reach it from a terminal. If you would like a hand setting that up, or handing the routine side of it over entirely, get in touch.