WP-CLI for developers

Install WordPress, manage plugins, content, users and the database from the terminal, and add commands of your own.

  • Intermediate
  • 10 min read
  • Updated 17 Oct 2026

What you will learn

  • Run common WP-CLI commands
  • Target a site with global parameters
  • Back up and query the database safely
  • Write a custom command

Before you start

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:

Shell
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

Shell
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

Shell
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

Shell
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

Shell
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

Shell
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

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

ParameterEffect
--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-themesLoad 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
--debugShow debugging output
--quietSuppress 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
<?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
<?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:

Shell
wp help myplugin
wp myplugin upcoming --limit=3
wp myplugin delete-past 2025-01-01 --dry-run

Things to notice:

  • Arguments: $args holds positional arguments such as <date>; $assoc_args holds 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() and WP_CLI::error() keep output consistent, and format_items() gives your command the same --format options as core commands.
  • @subcommand sets the command's name, so the method delete_past is called as delete-past.
  • A --dry-run flag 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-run where it exists, and export the database before commands that write to it.
  • Global parameters such as --path, --url, --user and --skip-plugins work with every command.
  • Register custom commands with WP_CLI::add_command() inside a WP_CLI check; the docblock defines the arguments.
  • Use WP_CLI::success(), WP_CLI::error(), format_items() and progress bars for clear, scriptable output.

Further reading