WP-CLI is the official command-line tool for WordPress. Anything you can click in the admin, and much you cannot, can be done with a command: install a site, update every plugin, create a hundred test posts, export the database. Commands are fast, repeatable and scriptable, which makes WP-CLI the backbone of local setup, deployments and maintenance.
Installing
WP-CLI is a single PHP archive (a .phar file). On macOS and Linux:
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
wp --info
Many local environments, including wp-env and several Windows and macOS bundles, ship with WP-CLI already, so check with wp --info first. Run commands from the WordPress root folder, the one that contains wp-config.php, or point to it with --path.
wp help lists every command, and wp help plugin install shows the options for one. The built-in help is always accurate for the version you have installed.
A new site in four commands
wp core download
wp config create --dbname=mysite --dbuser=root --dbpass=secret
wp db create
wp core install --url=https://mysite.test --title="My Site" --admin_user=admin --admin_email=admin@example.com
wp core install prints a generated password if you do not pass --admin_password. Never put production passwords in shell history or scripts.
Everyday commands
Core, plugins and themes
wp core version
wp core check-update
wp core update
wp core update-db
wp plugin list
wp plugin install query-monitor --activate
wp plugin update --all --dry-run
wp plugin deactivate myplugin
wp theme activate twentytwentyfive
wp core verify-checksums
wp plugin verify-checksums --all
The verify-checksums commands compare files with the official copies on WordPress.org. They are a quick way to spot modified core or plugin files after a suspected compromise.
Content and users
wp post list --post_type=myplugin_event --post_status=publish --fields=ID,post_title,post_date
wp post create --post_type=page --post_title="Contact" --post_status=publish
wp post generate --count=50 --post_type=myplugin_event
wp post meta get 42 myplugin_event_date
wp user list --role=administrator
wp user create sam sam@example.com --role=editor
wp post generate is perfect for testing pagination and performance with realistic volumes of content.
Options, caches, cron and rewrites
wp option get siteurl
wp option update blogdescription "Events in the city"
wp transient delete --expired
wp cache flush
wp cron event list
wp cron event run --due-now
wp rewrite flush
wp rewrite flush does the same as saving the Permalinks screen, which you need after registering a post type with a new slug.
The database
wp db export backup.sql
wp db import backup.sql
wp db query "SELECT ID, post_title FROM wp_posts WHERE post_status = 'publish' LIMIT 10"
wp db size --tables
wp db query runs raw SQL with full privileges and no undo. Use it for reading, and take a wp db export before anything that writes. wp db query does not know your table prefix, so check it with wp config get table_prefix.
wp search-replace changes URLs and strings across the whole database, handling serialised data correctly. Always run it with --dry-run first. The deployment lesson covers it in detail.
Running code
wp eval 'echo get_option( "home" ), PHP_EOL;'
wp shell
wp eval runs PHP with WordPress loaded. wp shell opens an interactive prompt, which is handy for trying out an API call. Both run with full access to the site, so treat them like the database.
Global parameters
These work with every command:
| Parameter | Effect |
|---|---|
--path=<path> | The WordPress folder to use |
--url=<url> | Which site to target, on Multisite |
--user=<id|login> | Run as this user, so capability checks pass or fail as they would for them |
--skip-plugins, --skip-themes | Load WordPress without plugins or the theme, useful when one of them causes a fatal error |
--ssh=<host> | Run the command on a remote server over SSH |
--debug | Show debugging output |
--quiet | Suppress informational messages |
Defaults can live in a wp-cli.yml file in the project root, for example path: web/wp, so you do not repeat them. The handbook also explains aliases, which let you name remote environments and run wp @staging plugin list.
Writing a custom command
Your plugin can add its own commands. They are ideal for jobs that are too slow or too risky for a web request: imports, data clean-ups, cache warming, migrations.
Register the command only when WP-CLI is running, so the class is never loaded for web requests. In the main plugin file:
<?php
/**
* Plugin Name: My Plugin
* Text Domain: myplugin
*/
defined( 'ABSPATH' ) || exit;
if ( defined( 'WP_CLI' ) && WP_CLI ) {
require_once __DIR__ . '/includes/class-myplugin-cli.php';
WP_CLI::add_command( 'myplugin', 'Myplugin_CLI' );
}
Each public method of the class becomes a subcommand. The docblock is not just documentation: WP-CLI reads the ## OPTIONS section to parse and validate arguments and to build wp help.
<?php
// includes/class-myplugin-cli.php
defined( 'ABSPATH' ) || exit;
/**
* Manage events for My Plugin.
*/
class Myplugin_CLI {
/**
* Lists upcoming events.
*
* ## OPTIONS
*
* [--limit=<number>]
* : How many events to show.
* ---
* default: 10
* ---
*
* [--format=<format>]
* : Output format.
* ---
* default: table
* options:
* - table
* - csv
* - json
* ---
*
* ## EXAMPLES
*
* wp myplugin upcoming --limit=5 --format=json
*
* @param array $args Positional arguments.
* @param array $assoc_args Named arguments.
*/
public function upcoming( array $args, array $assoc_args ): void {
$ids = get_posts(
array(
'post_type' => 'myplugin_event',
'post_status' => 'publish',
'posts_per_page' => absint( $assoc_args['limit'] ),
'fields' => 'ids',
'meta_key' => 'myplugin_event_date',
'orderby' => 'meta_value',
'order' => 'ASC',
'meta_query' => array(
array(
'key' => 'myplugin_event_date',
'value' => wp_date( 'Y-m-d' ),
'compare' => '>=',
'type' => 'DATE',
),
),
)
);
$items = array();
foreach ( $ids as $id ) {
$items[] = array(
'id' => $id,
'title' => get_the_title( $id ),
'date' => get_post_meta( $id, 'myplugin_event_date', true ),
);
}
WP_CLI\Utils\format_items( $assoc_args['format'], $items, array( 'id', 'title', 'date' ) );
}
/**
* Deletes events that took place before a date.
*
* ## OPTIONS
*
* <date>
* : Delete events before this date, as YYYY-MM-DD.
*
* [--dry-run]
* : Show how many events would be deleted, without deleting them.
*
* ## EXAMPLES
*
* wp myplugin delete-past 2025-01-01 --dry-run
*
* @subcommand delete-past
*
* @param array $args Positional arguments.
* @param array $assoc_args Named arguments.
*/
public function delete_past( array $args, array $assoc_args ): void {
$date = $args[0];
if ( ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $date ) ) {
WP_CLI::error( 'Use the date format YYYY-MM-DD.' );
}
$dry_run = (bool) WP_CLI\Utils\get_flag_value( $assoc_args, 'dry-run', false );
$ids = get_posts(
array(
'post_type' => 'myplugin_event',
'post_status' => 'any',
'posts_per_page' => -1,
'fields' => 'ids',
'meta_query' => array(
array(
'key' => 'myplugin_event_date',
'value' => $date,
'compare' => '<',
'type' => 'DATE',
),
),
)
);
if ( ! $ids ) {
WP_CLI::success( 'No past events found.' );
return;
}
if ( $dry_run ) {
WP_CLI::log( sprintf( '%d events would be deleted.', count( $ids ) ) );
return;
}
$progress = WP_CLI\Utils\make_progress_bar( 'Deleting events', count( $ids ) );
foreach ( $ids as $id ) {
wp_delete_post( $id, true );
$progress->tick();
}
$progress->finish();
WP_CLI::success( sprintf( 'Deleted %d events.', count( $ids ) ) );
}
}
Try it:
wp help myplugin
wp myplugin upcoming --limit=3
wp myplugin delete-past 2025-01-01 --dry-run
Things to notice:
- Arguments:
$argsholds positional arguments such as<date>;$assoc_argsholds named ones such as--limit=3. Defaults from the docblock are filled in, and a missing required argument stops the command with a usage message before your code runs. - Validation still matters. The docblock checks that a date was given, not that it is a date, so the method checks the format and calls
WP_CLI::error(), which prints the message and exits with a non-zero status. - Output helpers:
WP_CLI::log(),WP_CLI::success(),WP_CLI::warning()andWP_CLI::error()keep output consistent, andformat_items()gives your command the same--formatoptions as core commands. @subcommandsets the command's name, so the methoddelete_pastis called asdelete-past.- A
--dry-runflag for anything destructive lets you check what will happen before it does.
Commands run as no user by default, so current_user_can() returns false. Code that calls core functions which check capabilities may need --user=admin. The person at the terminal already has full access to the server, so the command itself does not need permission checks; it does need the same care with input as any other code.
For very large jobs, process posts in batches rather than loading every ID at once, and clear the in-memory cache between batches with wp_cache_flush_runtime() to stop memory growing. Avoid wp_cache_flush() on a live site with a persistent object cache, because it empties the whole cache for every visitor. The background jobs lesson returns to long-running work.
Summary
- WP-CLI manages core, plugins, themes, content, users, options, cron and the database from the terminal.
- Use
--dry-runwhere it exists, and export the database before commands that write to it. - Global parameters such as
--path,--url,--userand--skip-pluginswork with every command. - Register custom commands with
WP_CLI::add_command()inside aWP_CLIcheck; the docblock defines the arguments. - Use
WP_CLI::success(),WP_CLI::error(),format_items()and progress bars for clear, scriptable output.