If you are writing a deploy script, a CI job, or a provisioning step, the useful WP-CLI commands are not wp plugin install and wp user create. They are the ones that answer a question and set an exit code: wp plugin is-installed, wp cli has-command, wp option get, wp core is-installed.
The big ranking guides do not cover them. Kinsta's WP-CLI guide runs to roughly 4,000 words without mentioning exit codes, --format, --skip-plugins or wp eval (read 2026-09-13). ChemiCloud's is longer still and mentions --skip-plugins in one line, with nothing on exit codes or output formats.
That is the gap. A command catalogue tells you what to type. It does not tell you how to read the answer back.
Everything below was checked against the WP-CLI source on GitHub on 2026-09-13, in wp-cli/wp-cli, wp-cli/extension-command, wp-cli/entity-command and wp-cli/db-command.
What exit code does WP-CLI actually return?
WP_CLI::error() exits with 1. That is the whole story for failures, unless a command passes an integer to the second argument, which almost none do.
$return_code = false;
if ( true === $exit ) {
$return_code = 1;
}
So in a script, wp plugin install foo || exit 1 works, and anything more granular than "it failed" does not exist. Do not write case $? in 2) ... esac.
The interesting commands are the ones that use WP_CLI::halt() instead, which sets the code deliberately rather than as a side effect of an error.
The commands that exist only to be tested
These four print nothing useful and are meant for if. They are the closest thing WP-CLI has to a scripting API.
# Is WordPress installed at all? 0 if yes.
wp core is-installed
# Is this a multisite network? 0 if yes.
wp core is-installed --network
# Is the plugin on disk? 0 if yes, 1 if not.
wp plugin is-installed woocommerce
# Is it active? 0 if yes.
wp plugin is-active woocommerce
# Does this command exist in this install? 0 if yes.
wp cli has-command "db query"
wp plugin is-installed is four lines in Plugin_Command.php, and both branches are a halt():
if ( $this->fetcher->get( $args[0] ) ) {
WP_CLI::halt( 0 );
} else {
WP_CLI::halt( 1 );
}
wp cli has-command is the one to reach for before calling anything from a package or a plugin-provided command. The WP-CLI source documents the pattern itself: if ! $(wp cli has-command doctor); then wp package install wp-cli/doctor-command; fi.
wp plugin is-active has a branch worth knowing about. If a plugin is listed in active_plugins but its file is missing from disk, it prints Plugin 'x' is marked as active but the plugin file does not exist. as a warning and still exits 1. That is the signature of a half-finished deploy, and a script that only reads the exit code will see "not active" and never see the warning.
The wp option get trap
This one costs people an afternoon. wp option get decides the option is missing by comparing to boolean false:
$value = get_option( $key );
if ( false === $value ) {
WP_CLI::error( "Could not get '{$key}' option. Does it exist?" );
}
get_option() returns false for an option that does not exist. It also returns false for an option that exists and holds boolean false. WP-CLI cannot tell those apart, so a real, present option reports "Does it exist?" and exits 1.
If your script branches on wp option get my_flag, and the plugin stores that flag as a boolean, the script takes the missing-option branch every time the flag is off.
The fix is to ask the database instead of asking WP-CLI:
wp db query "SELECT COUNT(*) FROM $(wp db prefix)options WHERE option_name = 'my_flag'" --skip-column-names
Output formats, and the two that are not tables
Almost every list command takes --format. The values that matter to a script are count and ids, because they need no parsing at all.
# A number, on its own line
wp plugin list --status=active --format=count
# Space-separated ids, ready to pipe into xargs
wp post list --post_type=page --format=ids
# Structured, for jq
wp plugin list --status=active --fields=name,version --format=json
--format=csv is there too. Reach for json over csv when a value might contain a comma, which for plugin descriptions it will.
Pair --fields with --format=json rather than taking the default set. The default field list changes between WP-CLI versions, and a script that indexes into it breaks quietly on an upgrade.
--skip-plugins is a bisect tool, not a performance flag
Most guides list --skip-plugins as a way to make commands faster. Its real use is finding which plugin broke the site, without deactivating anything.
It works by filtering the active_plugins option in memory at runtime:
$hooks = [
'pre_site_option_active_sitewide_plugins',
'site_option_active_sitewide_plugins',
'pre_option_active_plugins',
'option_active_plugins',
];
Nothing is written. The database still says every plugin is active, so the site keeps serving normally to visitors while you run the command.
It takes an optional comma-separated list of plugin folder names, so you can bisect:
# Everything off
wp --skip-plugins --skip-themes option get home
# Just the two suspects off
wp --skip-plugins=woocommerce,elementor option get home
Two limits. The names it matches are folder names, resolved from the plugin path, not the display names you see in the admin. And must-use plugins are not in active_plugins, so --skip-plugins does not touch them. If the breakage survives --skip-plugins, look in wp-content/mu-plugins.
wp db query is not a WordPress command
wp db query shells out to the mysql binary and hands it your SQL. Two consequences follow from that, and both are in the command's own documentation.
First, --url has no effect. On multisite you must write the numbered table prefix yourself:
# Wrong on multisite: --url is ignored
wp db query 'SELECT option_value FROM wp_options WHERE option_name="home"' --url=site2.example.com
# Right: the site's own table
wp db query 'SELECT option_value FROM wp_2_options WHERE option_name="home"' --skip-column-names
Second, it reads from STDIN, so wp db query < migration.sql works and is the cleanest way to run a file of SQL.
--skip-column-names is what turns the ASCII table into a value you can assign to a shell variable. Without it you get box-drawing characters in your variable.
Running these against a throwaway install
All of the above is easier to confirm when the site you are confirming it on does not matter. A scripted wp option get branch, a --skip-plugins bisect, a wp db query with the wrong prefix: each of those is a five-second experiment on a disposable install and a bad afternoon on production.
On SandyWP, every sandbox has WP-CLI available over SSH and SFTP, and the CLI can run a single command without opening a session:
sandywp ssh my-site --cmd "wp plugin list --status=active --format=count"
Verified against our own docs: --cmd runs one command and exits, and SSH access needs a sandbox you own on a paid plan.
If you want to see what a failing script sees rather than what you hope it sees, turn on debug mode first. A PHP notice emitted before WP-CLI's output will end up inside the string your script is parsing.
When WP-CLI is the wrong tool
Two honest limits.
If you need to know whether a change is safe, WP-CLI will not tell you. wp plugin update --all exits 0 on a site whose checkout page is now blank. Run the update on a copy and look at the pages first.
And if a plugin registers its behaviour through the admin UI only, with no command and no option, there is nothing for wp option get to read. That is a real category, and the answer is a browser on a disposable copy, not a cleverer script.
The short list
If you keep one thing from this post, keep these five:
wp core is-installed # 0 if WordPress is there
wp plugin is-active <name> # 0 if active
wp cli has-command "<command>" # 0 if the command exists
wp plugin list --status=active --format=count # a bare number
wp --skip-plugins=<a,b> <command> # bisect without deactivating
Every one of them sets an exit code or prints one unambiguous value. That is the whole difference between a command catalogue and a scripting reference.
