# 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.

Source: https://vpsnine.com/tutorials/wp-cli-manage-wordpress · Updated: 2026-10-21

Difficulty: intermediate · Time: about 35 minutes · Tested on: Ubuntu 24.04 LTS

Prerequisites:

- 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](https://vpsnine.com/help/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:

```bash
sudo apt install -y php-cli curl
```

Download WP-CLI, check it against the published checksum, and put it on your path as `wp`:

```bash
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/wp
```

**Check 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:

```text
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:

```bash
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 ~/.bashrc
```

All 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.**

```bash
wp core version
wp option get siteurl
```

They 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:

```bash
wp core check-update
wp plugin list --update=available
wp theme list --update=available
```

Export the database first (next section), then update everything:

```bash
wp core update
wp core update-db
wp plugin update --all
wp theme update --all
wp language core update
```

`wp 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:

```bash
wp plugin auto-updates enable --all
```

**Check 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

```bash
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 twentytwentyfive
```

Unused 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:

```bash
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:

```bash
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 --all
```

`wp 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

```bash
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:

```bash
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](https://vpsnine.com/help/backups-you-should-run) 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:

```bash
wp search-replace 'https://example.com' 'https://www.example.com' --skip-columns=guid --dry-run --report-changed-only
```

It lists each table and column with the number of replacements. If it looks right, run it for real, without `--dry-run`:

```bash
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:

```bash
wp config set DISALLOW_FILE_EDIT true --raw
```

**Replace 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:

```bash
wp config shuffle-salts
```

**Check 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:

```bash
wp core verify-checksums
wp plugin verify-checksums --all
```

Both 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:

```bash
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/wordpress
```

`wp 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](https://vpsnine.com/help/php2shell-check-and-patch-php) 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.

```bash
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.php
```

`wp-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:

```bash
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 ~/.bashrc
```

The 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`:

```text
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 --quiet
```

Update 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:

```bash
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-checksums
```

Use 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](https://vpsnine.com/help/wordpress-on-a-vps) for the nginx, caching and HTTPS side.
- [Install PHP and PHP-FPM with nginx](https://vpsnine.com/tutorials/install-php-fpm-nginx-ubuntu) to see how the PHP layer fits together.
- `wp help <command>` documents every command on the server itself; the [WP-CLI command reference](https://developer.wordpress.org/cli/commands/) is the same online.
