WP-CLI for People Who Live in the Terminal
WP-CLI commands for WordPress migrations, backups, lockouts, plugin and core updates, remote SSH tasks, and safe search-replace on serialized data.
WP-CLI is the command-line interface to a WordPress install. It saves clicks, but the real reason to learn it is the set of jobs the dashboard has no screen for: rewriting serialized option data during a domain change, running arbitrary PHP against a live site, and updating ten installs from one prompt.
Most people meet it in an emergency. A plugin update takes the admin down, the dashboard will not load, and FTP plus phpMyAdmin is suddenly the only way back in. That route works, but it is slow and it takes some nerve.
This article is organised by job, not by namespace. Each section is a task that is painful or impossible in the dashboard, followed by the command that does it and the flags that make it safe.
Key Takeaways
wp search-replaceunserializes PHP data, applies the replacement, and reserializes it, which is why it can rewrite widget settings and plugin options that a raw SQLREPLACE()would corrupt.- Run every replacement with
--dry-runfirst, then run the identical command without the flag. - Exclude the
guidcolumn with--skip-columns=guid, because feed readers use a post’s guid to work out whether they have already shown it. --ssh=[<scheme>:][<user>@]<host>[:<port>][<path>]proxies a command to a remote install, and the remote machine needs its own copy of WP-CLI that answers towp.- None of these commands prompt for confirmation and none can be undone, so
wp db exportcomes first.
These Commands Execute Immediately
There is no confirmation dialog, no preview screen, and no undo. wp search-replace writes to every matching row the moment you press return. wp plugin deactivate --all deactivates everything on a production site as readily as on a laptop. The only rollback you have is the database export you took beforehand, so take one.
How Do You Change a Domain Without Breaking Serialized Data?
wp search-replace is the right tool for a domain change: it reads serialized PHP properly and leaves primary keys alone, neither of which a plain SQL find-and-replace manages. The reason is the storage format: PHP’s serialize() records a string as its byte length followed by the string itself.
a:1:{s:3:"url";s:27:"https://staging.example.com";}
A blind SQL UPDATE ... REPLACE() rewrites the URL to https://example.com and leaves the 27 untouched. The declared length no longer matches the payload, PHP can no longer unserialize the value, and the widget or plugin option that lived in it silently reverts to nothing. WP-CLI unserializes the structure, replaces inside it, and reserializes with correct lengths.
Escalate in three steps:
# 1. report what would change; writes nothing
wp search-replace 'https://staging.example.com' 'https://example.com' \
--skip-columns=guid --dry-run
# 2. optional: write the result to a SQL file instead of the database
wp search-replace 'https://staging.example.com' 'https://example.com' \
--skip-columns=guid --export=migration.sql
# 3. apply it
wp search-replace 'https://staging.example.com' 'https://example.com' \
--skip-columns=guid
--dry-run runs the whole job and prints the report, then throws the changes away. --export sends the result to a SQL file and leaves the live database untouched, so you can read the diff or apply it somewhere else. Skip the guid column because WordPress treats a post’s guid as fixed for the life of the post: change it and feed readers may show your whole back catalogue as new.
For awkward nested data, add --precise. By default the command uses fast SQL queries and switches to PHP automatically for columns containing serialized data; --precise forces PHP for every column, which is slower but more reliable against complex serialized structures. Regex mode is substantially slower too, so reach for it only when a literal string will not do.
How Do You Update Plugins and Core Across Several Sites?
One command updates everything that has an update available, with no dashboard pagination and no per-plugin checkboxes:
wp plugin update --all
wp core update
wp core update-db
wp core update-db runs WordPress’s database update routine, which is the step the dashboard performs for you on the upgrade screen after a core update. Run it after wp core update so the upgrade finishes rather than half-finishing.
Combined with the aliases covered below, the same line becomes wp @all plugin update --all and hits every install you maintain in sequence.
Export Before, Import After
wp db export shells out to mysqldump and picks up the database host, name, user and password from wp-config.php, so you never type connection details. Give it an explicit filename; omit one and it writes {dbname}-{Y-m-d}-{random-hash}.sql.
wp db export backup-$(date +%Y%m%d-%H%M%S).sql
Restoring is the mirror image:
wp db import backup-20250413-141055.sql
wp db import takes either a filename or piped input, so you can send an export straight from one host to another over ssh. For a longer-term strategy than a single dump before a risky change, OpenReplay’s WordPress backup articles cover scheduling and offsite storage.
How Do You Get Back Into a Site You Are Locked Out Of?
Three commands cover almost every lockout, in the order you would run them under pressure. Create a fresh administrator, reset an existing user’s password, or take the plugins out of the picture entirely:
wp user create ops ops@example.com --role=administrator
wp user reset-password admin --show-password --skip-email
wp plugin deactivate --all
wp user reset-password generates a new password; --show-password prints it to the terminal and --skip-email stops the notification going to an inbox you may not control. wp plugin deactivate accepts --all to deactivate everything, plus --exclude=<name> to keep a comma-separated list active.
When a fatal error in a plugin is what broke the admin, WP-CLI may fail to bootstrap for the same reason the site does. The --skip-plugins global parameter stops all plugins, or a named list, from loading for the duration of the command:
wp plugin deactivate broken-plugin --skip-plugins
Skipping does not change stored state; a plugin skipped this way still reports as active. It only buys you a working bootstrap so the deactivation can run. It also does not help when the fatal code sits in an mu-plugin, because WP-CLI loads mu-plugins either way. This is the moment most maintainers first need WP-CLI, and it is quicker than opening an FTP client and renaming plugin directories. Once the admin is back, the diagnostic half of the job is covered in OpenReplay’s article on the WordPress white screen of death.
Running One-Off PHP With wp eval
wp eval executes arbitrary PHP against a fully loaded WordPress install. There is no dashboard equivalent, and that is the point: any function a plugin registers, any option, any query, becomes a one-liner.
wp eval 'echo get_option( "siteurl" );'
wp eval 'echo count( get_users( [ "role" => "administrator" ] ) );'
Anything longer belongs in a file. wp eval-file takes the path to a PHP file, hands any extra positional arguments to the script as $args, and will skip the WordPress bootstrap altogether if you pass --skip-wordpress. Your code runs inside a method, so every global you touch needs its own global line.
There is no dry run for wp eval. Whatever the script writes, it writes. That is the clearest argument for the export.
How Do You Run WP-CLI Against a Remote Host?
This is where the tool stops being a convenience. WP-CLI’s --ssh global parameter takes the form --ssh=[<scheme>:][<user>@]<host>[:<port>][<path>] and works by handing your command to the ssh binary, which passes it to the WP-CLI sitting on the far end.
wp --ssh=dev_user@example.com:2222~/webapps/production plugin list
| Component | Value here | Default if omitted |
|---|---|---|
| scheme | (omitted) | ssh |
| user | dev_user | your current system user |
| host | example.com | required |
| port | 2222 | 22 |
| path | ~/webapps/production | the ssh user’s home directory |
The path takes no separator. Write it straight after the port, or straight after the host if you left the port out, and start it with / or ~. Besides ssh, the handbook config reference documents vagrant, docker, docker-compose and docker-compose-run. The last of those starts a fresh container with docker-compose run rather than using one that is already up.
One prerequisite is absolute: the remote server needs its own WP-CLI, and it has to answer to wp. A wp that works when you log in by hand can still come back as command-not-found over --ssh, because the shell that runs a remote command does not build the same $PATH. Most distributions put a guard near the top of ~/.bashrc that exits early when the shell is not interactive, so any PATH line below it never runs; zsh reads ~/.zshenv rather than ~/.zshrc in that situation. The fix is to set $PATH explicitly on the remote side.
Typing that string twice is enough. Register aliases in your project’s wp-cli.yml or your global ~/.wp-cli/config.yml:
@prod:
ssh: deploy@example.com~/webapps/production
@stage:
ssh: deploy@staging.example.com~/webapps/staging
@all:
- @prod
- @stage
wp @prod plugin update --all
wp @all core check-update
An alias group runs one invocation against several installs, which is the difference between maintaining ten client sites and logging into ten dashboards. For a local install that is not in your current directory, the --path global parameter tells WP-CLI where the WordPress files are:
wp --path=/var/www/example.com/htdocs plugin update --all
Where to Go Next
The single idea worth carrying out of this: WP-CLI understands WordPress data structures, and mysql and phpMyAdmin do not, which is why a domain change belongs in wp search-replace and nowhere else. Pick the next migration you have scheduled, write the --dry-run line, read the report, and run wp db export before you drop the flag. Everything above is irreversible the instant you press return.
FAQs
Does wp search-replace update every site in a multisite network?
No. It works on the tables that WordPress itself registers, so on multisite you get the current site's tables only, unless you add --network. To reach every table in the database, whatever its prefix and whether or not WordPress knows about it, use --all-tables, which takes priority over --network and --all-tables-with-prefix. On a network, add --url as well so WP-CLI boots into the right site.
Why does WP-CLI refuse to run as root?
WP-CLI stops with a YIKES error when it spots the root user. Everything inside the install, including plugins and themes you did not write, would inherit root's reach over the server, so one piece of hostile code could take the whole machine. The --allow-root flag skips the check and containers running as root often need it, but the project advises against it. Run as the system user that owns the WordPress files instead.
What does the error 'This does not seem to be a WordPress installation' mean?
WP-CLI found no WordPress core files where it looked, so it never bootstrapped. Run the command from the directory containing wp-admin, wp-content and wp-includes, or point it at the install with the --path global parameter. Pass the value in the equals form, --path=/var/www/html, because a space-separated argument leaves the flag without a value and the same error repeats.
Does WP-CLI run on Windows?
WP-CLI is built for a UNIX-like environment such as Linux, macOS, FreeBSD or Cygwin, and it is only partly supported on Windows itself, so WSL or Cygwin is the reliable route on a Windows machine. It also needs WordPress 4.9 or newer, and anything behind the current WordPress release may not work in full.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k