WP-CLI tutorial: manage and harden WordPress from the command line
Install WP-CLI, then update WordPress, manage plugins and users, move a site to a new domain, back up the database and lock the site down, all over SSH.
Tested on Ubuntu 24.04 LTS
Before you start
- A WordPress site on a server you can SSH into (Ubuntu 24.04 or Debian 12 or 13)
- Root or a user with sudo
- PHP's command-line binary (php-cli)
WP-CLI is the official command-line tool for WordPress: you install it as a single file (wp), then run commands such as wp core update, wp plugin update --all and wp db export over SSH instead of clicking through the dashboard. This tutorial installs it, shows the commands you'll use every week, and uses them to harden the site: no file editor in the dashboard, fresh salts, checksum checks, real cron and, optionally, files the web server can't change.
It assumes WordPress is already installed. If it isn't, host WordPress on a VPS sets it up with nginx, PHP-FPM and MariaDB; the paths below (/var/www/wordpress, files owned by www-data) match that guide. Change them if your site lives elsewhere.
Install WP-CLI
WP-CLI needs PHP's command-line binary. On Ubuntu and Debian:
sudo apt install -y php-cli curlDownload WP-CLI, check it against the published checksum, and put it on your path as wp:
cd /tmp
curl -fsSLO https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
curl -fsSLO https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar.sha512
echo "$(cat wp-cli.phar.sha512) wp-cli.phar" | sha512sum -c
chmod +x wp-cli.phar
sudo mv wp-cli.phar /usr/local/bin/wpCheck it worked. sha512sum -c prints wp-cli.phar: OK, and wp --version prints the version (WP-CLI 2.12.0 when we wrote this). Update it later with sudo wp cli update.
Run it as the site's owner
Run WP-CLI as the user that owns the WordPress files, never as root. As root, WP-CLI refuses with:
Error: YIKES! It looks like you're running this as root. You probably meant to run this as the user that your WordPress installation exists under.It's right to: WP-CLI loads the site's plugins, so a bad plugin would run with root's power, and files it creates would belong to root, which then breaks updates from the dashboard.
With the files owned by www-data, give WP-CLI somewhere to keep its download cache (www-data's home is /var/www, which it can't write to), and add an alias to your own ~/.bashrc so every wp command runs as www-data against the site:
sudo install -d -o www-data -g www-data /var/www/.wp-cli
echo "alias wp='sudo -u www-data wp --path=/var/www/wordpress'" >> ~/.bashrc
source ~/.bashrcAll commands below use this alias. Without it, write them out in full: sudo -u www-data wp --path=/var/www/wordpress core version.
Check it worked.
wp core version
wp option get siteurlThey print the WordPress version and your site's address. Error: This does not seem to be a WordPress installation. means the --path is wrong.
Update WordPress, plugins and themes
See what's out of date:
wp core check-update
wp plugin list --update=available
wp theme list --update=availableExport the database first (next section), then update everything:
wp core update
wp core update-db
wp plugin update --all
wp theme update --all
wp language core updatewp core update-db runs the database upgrade a new WordPress version sometimes needs; the dashboard does this after a click, WP-CLI waits for you to ask. wp plugin update --all --dry-run lists what would change without changing it.
To let WordPress update the plugins by itself between your visits:
wp plugin auto-updates enable --allCheck it worked. wp core check-update says Success: WordPress is at the latest version., and wp plugin list shows none in the update column for every plugin.
Manage plugins, themes and users
wp plugin install classic-editor --activate # install and turn on, by its wordpress.org slug
wp plugin deactivate classic-editor
wp plugin delete classic-editor
wp theme activate twentytwentyfiveUnused plugins and themes are still code on the server that an attacker can reach, even when they're turned off. List the inactive ones, check the list, then delete them:
wp plugin list --status=inactive --field=name
wp plugin delete $(wp plugin list --status=inactive --field=name)
wp theme delete $(wp theme list --status=inactive --field=name)Keep one default theme (twentytwentyfive or similar) if your active theme is a child theme or you want something to fall back to.
Users:
wp user list --role=administrator --fields=ID,user_login,user_email
wp user create priya priya@example.com --role=editor
wp user update maya --user_pass='a-long-new-password'
wp user session destroy maya --allwp user create prints the new user's generated password. wp user session destroy --all logs a user out everywhere, which is what you want after a password change you made because of a suspected leak. Anyone listed as administrator who shouldn't be is the first thing to remove after a break-in.
Back up and restore the database
wp db export - | gzip > ~/wp-db-$(date +%F).sql.gz- writes the dump to standard output, so it lands in your own home folder (compressed) rather than inside the web root. To restore it:
gunzip -c ~/wp-db-2026-10-21.sql.gz | wp db import -A full backup is the database plus wp-content (uploads, plugins, themes), and it belongs on another machine: back up your server covers copying both off the server every night.
Check it worked. gunzip -c ~/wp-db-*.sql.gz | head -5 shows the start of a MariaDB dump, and wp db size --human-readable shows how big the live database is, for comparison.
Move the site to a new domain
WordPress stores its own address, in full, all over the database: in options, in post content, in serialized plugin settings. A plain SQL REPLACE breaks the serialized values; wp search-replace rewrites them correctly.
Export the database first. Then look before you change anything:
wp search-replace 'https://example.com' 'https://www.example.com' --skip-columns=guid --dry-run --report-changed-onlyIt lists each table and column with the number of replacements. If it looks right, run it for real, without --dry-run:
wp search-replace 'https://example.com' 'https://www.example.com' --skip-columns=guid
wp cache flush--skip-columns=guid leaves post GUIDs alone: they're permanent IDs that feed readers use, not links. Include the scheme (https://) in both strings so you don't half-replace other text.
Check it worked. wp option get home and wp option get siteurl print the new address. Update the domain in your nginx config and certificate as well.
Harden the site
Each of these is one command. Together they close the most common ways a WordPress site gets taken over.
Turn off the theme and plugin file editor. Anyone who gets an administrator's password can otherwise paste PHP straight into a theme file from the dashboard:
wp config set DISALLOW_FILE_EDIT true --rawReplace the salts. The keys in wp-config.php sign login cookies. New ones log everyone out and make stolen cookies useless. Do it after any suspected break-in, or if your wp-config.php was ever shared:
wp config shuffle-saltsCheck the files against wordpress.org. This compares every core file, and every plugin from the plugin directory, with the published checksums. A modified or extra file is a classic sign of a backdoor:
wp core verify-checksums
wp plugin verify-checksums --allBoth should end with Success. Premium plugins that aren't on wordpress.org can't be checked and are reported as skipped.
Run WordPress's scheduled tasks from real cron. WP-Cron normally runs scheduled jobs (publishing, update checks, plugin tasks) only when someone visits the site, so a quiet site misses them and a busy one runs the check on every request. Turn that off and run it every five minutes instead:
wp config set DISABLE_WP_CRON true --raw
echo '*/5 * * * * www-data /usr/local/bin/wp --path=/var/www/wordpress cron event run --due-now --quiet' | sudo tee /etc/cron.d/wordpresswp cron event run --due-now prints Success: Executed a total of N cron events. when you run it by hand.
Keep PHP itself up to date as well: WordPress is only as safe as the PHP under it. The PHP2Shell guide shows how to check and patch it.
Optional: make the code read-only for the web server
The setup above lets www-data, the user PHP runs as, write every file of the site. That's what makes one-click updates in the dashboard work, and it's also what lets a hole in one plugin rewrite all the others. You can take that away: files owned by a separate user, with only wp-content/uploads writable by the web server.
sudo useradd -m -s /bin/bash wpowner
sudo chown -R wpowner:wpowner /var/www/wordpress
sudo chown -R www-data:www-data /var/www/wordpress/wp-content/uploads
sudo find /var/www/wordpress -type d -exec chmod 755 {} +
sudo find /var/www/wordpress -type f -exec chmod 644 {} +
sudo chgrp www-data /var/www/wordpress/wp-config.php
sudo chmod 640 /var/www/wordpress/wp-config.phpwp-config.php holds the database password, so only its owner and the web server's group can read it.
Then tell WordPress to stop offering installs and updates it can no longer do, and switch your alias to the new owner:
sudo -u wpowner wp --path=/var/www/wordpress config set DISALLOW_FILE_MODS true --raw
sed -i "s/sudo -u www-data wp/sudo -u wpowner wp/" ~/.bashrc && source ~/.bashrcThe trade-off: with DISALLOW_FILE_MODS, WordPress no longer installs anything by itself, including its automatic security releases. WP-CLI ignores the setting, so updates are now your job: run them weekly (or daily) from the owner's crontab, sudo crontab -u wpowner -e:
30 4 * * * /usr/local/bin/wp --path=/var/www/wordpress core update --minor --quiet && /usr/local/bin/wp --path=/var/www/wordpress plugin update --all --quietUpdate the cron line from the previous section to run as wpowner too. Some plugins (caching and security plugins especially) want to write their own files outside uploads; give them their folder only, not the whole tree.
Check it worked. sudo -u www-data touch /var/www/wordpress/wp-content/plugins/test fails with Permission denied, while uploading an image in the dashboard still works.
Fix the common problems
Error: Error establishing a database connection. WP-CLI reads the same wp-config.php as the site, so the database settings are wrong for both, or MariaDB is down: systemctl status mariadb. If the site works in a browser but WP-CLI fails, DB_HOST may be localhost and the CLI's PHP can't find the socket; check php -i | grep mysqli.default_socket.
Warning: Failed to create directory '/var/www/.wp-cli/cache/': mkdir(): Permission denied. The cache folder is missing. Create it as in "Run it as the site's owner", or ignore it: it only means downloads aren't cached.
wp core verify-checksums lists many File doesn't exist: wp-includes/php-ai-client/... lines. We saw this on WordPress 7.1.3 installed with wp core download (WP-CLI 2.12.0): files with very long paths didn't come out of the archive. Sites installed from the latest.tar.gz download, as in the WordPress guide, verify cleanly. To repair, replace the two core folders from the official archive for your version:
V=$(wp core version)
cd /tmp && curl -fsSLO "https://wordpress.org/wordpress-$V.tar.gz" && tar xzf "wordpress-$V.tar.gz"
sudo rsync -a --delete --chown=www-data:www-data /tmp/wordpress/wp-admin/ /var/www/wordpress/wp-admin/
sudo rsync -a --delete --chown=www-data:www-data /tmp/wordpress/wp-includes/ /var/www/wordpress/wp-includes/
wp core verify-checksumsUse the right owner in --chown if you made the code read-only.
PHP Fatal error: Allowed memory size of ... exhausted. Some commands (big search-replace runs, imports) need more memory than the CLI's default. Run them with more: sudo -u www-data php -d memory_limit=512M /usr/local/bin/wp --path=/var/www/wordpress ....
Next steps
- Host WordPress on a VPS for the nginx, caching and HTTPS side.
- Install PHP and PHP-FPM with nginx to see how the PHP layer fits together.
wp help <command>documents every command on the server itself; the WP-CLI command reference is the same online.