Skip to content

WP-CLI commands that pay for themselves

The six WP-CLI commands that save us 1-3 hours per WordPress engagement, with the dollar value attached and the cron + bash glue we run them in.

We log every hour against client engagements, and the same six WP-CLI invocations show up as the biggest time savers across audits, migrations, restorations, and ongoing retainer work. Not the cool ones from conference talks. Just the boring ones that move the needle on a billable week.

If you spend this much time in a terminal, the control panel debate matters less than people think. We get into where it does matter in how Plesk and cPanel actually compare.

This post is the short version of the cheat sheet we hand to junior consultants on day one, with the dollar value attached.

What “pays for itself” actually means here

We charge fixed prices on most WordPress engagements: $390 for a security audit, $890 for a migration, $180 minimum for an incident restoration. Inside those fixed prices, every hour we shave off the manual work is margin we keep. Across the last 40 engagements, the six commands below each save us 1-3 hours per job, sometimes more on bigger sites. That is real money on a fixed-price book of business, and it adds up to roughly $2,000 to $4,000 of recovered margin per month at our current cadence.

WP-CLI is also the difference between a junior consultant being useful on day one and being useful in month three. Most of the workflow below can be templated as bash scripts that anyone on the team can run safely.

1. wp search-replace with --dry-run and --export

The migration command, but used safely.

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

This is the single command that has prevented more “we broke the site” tickets than anything else. We run it before every domain move, every staging push, and every TLS upgrade. The --dry-run flag prints what would change without writing anything. The --report-changed-only flag trims the output so it actually fits on a terminal. The --skip-columns=guid flag stops you from rewriting historical post GUIDs, which breaks feed readers and external links.

Once the dry-run looks clean, we rerun without --dry-run, then immediately run a content spot-check with wp post list --post_status=publish --format=csv plus a curl of the homepage to confirm nothing surprising landed.

Saves us 1-2 hours per migration in not having to roll back, plus the customer-facing trust we keep by not breaking permalinks.

2. wp db query for surgical SQL when search-replace cannot reach

Some artefacts of an old install live in JSON blobs inside wp_options or in serialized PHP arrays inside theme settings. wp search-replace handles serialized data well, but it cannot fix everything (notably theme options with custom serialization, custom post-meta with embedded JSON references).

wp db query "SELECT option_name FROM wp_options \
  WHERE option_value LIKE '%old.example.com%' LIMIT 50;"

We use this as a sweep after every migration to find leftover references. It surfaces things wp search-replace glossed over. Pairs well with wp option get themename_options --format=json | jq . for inspection.

Saves us 30-60 minutes per migration on hard-to-find leftover references, and prevents the “why is one widget still pointing at staging” support ticket two days later.

3. wp plugin list --format=json for retainer status snapshots

Every WordPress retainer client gets a monthly patch report. The data lives in WP-CLI, two minutes away:

wp plugin list --format=json --fields=name,status,version,update,update_version \
  > /tmp/plugin-state.json
wp theme list --format=json --fields=name,status,version,update,update_version \
  > /tmp/theme-state.json
wp core check-update --format=json > /tmp/core-state.json

We pipe these into a tiny Python script that renders the monthly retainer PDF — what was updated, what could not be, why. Before this we built the report by hand and it took an hour. Now it takes about 90 seconds plus the time to write the human summary.

Saves us 45 minutes per client per month. At 12 active retainer clients, that is about 9 hours back per month, or roughly $1,800 of recovered consultant time.

4. wp eval-file for one-off fixes that should not be in code

Sometimes the right answer to “I need to backfill an ACF field on 3,000 posts” or “I need to flip user roles for half the editors” is a five-line PHP script, run once, deleted after. We do not want this in the theme, in a plugin, or in version control.

wp eval-file /tmp/backfill_field.php

The PHP file uses normal WP functions (get_posts, update_post_meta, etc.). We write it, dry-run it (print first, write second), then run it for real and delete the file.

This replaces the “let me ssh in and write a wp-admin tools-page that nobody else will ever use” anti-pattern. Saves 1-3 hours per data migration depending on complexity.

Important: always run wp db export before the eval-file, every time.

5. wp transient delete-all and wp cache flush

The single most useful support-ticket-killer command. About a third of “the site looks broken” tickets we get from new retainer clients are resolved by:

wp transient delete-all
wp cache flush
wp rewrite flush

Plus a curl with cache headers checked. If the issue persists past these three, then we start debugging. If it does not, we close the ticket and explain to the client what a transient is.

This sequence is in our standard support runbook and probably saves us 20-30 hours a month across the retainer book combined. The clients still see this as “fast support”, which is a brand point we like.

6. wp media regenerate --yes --skip-delete

After every theme change, after every CDN switch, after every image-size addition in functions.php, this is what makes the site stop returning old thumbnail dimensions:

wp media regenerate --yes --skip-delete

The --yes flag is the “do not prompt” flag, which matters on a site with 8,000 attachments. The --skip-delete flag means we keep the previously generated thumbnails around in case the new ones turn out wrong, which has saved us at least once.

Saves us about 45 minutes after each theme-change engagement, plus a follow-up ticket from a client noticing the wrong-size hero image two days later.

Three honorable mentions

wp cron event list and wp cron event run <hook> — for diagnosing why a scheduled job did not fire. WordPress cron is pseudocron and silently breaks under heavy traffic. This command tells you what is scheduled and lets you manually trigger.

wp user list --role=administrator --field=user_login — runs in our security audits as the first sweep for unfamiliar admin accounts. Followed by wp user list --role=administrator --fields=user_login,user_email,user_registered. Surfaces sketchy accounts in seconds.

wp post list --post_status=publish --meta_key=_some_field --format=count — for “how many posts have X tagged?” reporting, often used as the data input for ACF backfill scripts.

What we used to lean on, and stopped

wp config set for changing database credentials in wp-config.php — useful, but if you are touching production credentials, you should be writing them by hand because the audit trail matters.

wp option update siteurl and home — used to be our go-to before a migration. Now we let wp search-replace handle the whole pass instead, so the URL changes happen in one transactional sweep rather than two steps.

wp role create and wp cap add — once useful, now we generally manage roles via Members or User Role Editor in the admin so the configuration is visible and auditable, not buried in shell history.

Cron + bash glue is where this really pays off

Five of the six commands above end up inside a small bash script that runs from cron weekly. We push the plugin/theme/core state snapshot into a long-running log, then diff it week-over-week to catch silent version downgrades or unexpected reactivations. That diff is the single most valuable input we have for the security audit, and it costs us about 20 minutes of bash setup per client to enable.

Sample skeleton we hand new clients on day one:

#!/usr/bin/env bash
set -euo pipefail
SITE_DIR="/home/example.com/public_html"
LOG_DIR="/var/log/wp-state/example.com"
mkdir -p "$LOG_DIR"

cd "$SITE_DIR"
wp --allow-root plugin list --format=json --fields=name,status,version,update \
  > "$LOG_DIR/plugins-$(date +%F).json"
wp --allow-root core check-update --format=json \
  > "$LOG_DIR/core-$(date +%F).json"
wp --allow-root user list --role=administrator \
  --fields=user_login,user_email,user_registered --format=csv \
  > "$LOG_DIR/admins-$(date +%F).csv"

We park this in /etc/cron.weekly/wp-state-example and forget about it. The weekly snapshot lives on the server, gets rotated, and feeds the next security review without any human effort.

Where this fits

The point of WP-CLI is not the long list of commands. It is the few you actually use every week, made boring on purpose, scripted, and quietly running while you do the harder work. The list above is what we rely on. If you want this set up properly on a server you run yourself, our WordPress support retainer includes the cron snapshotting, the patch reports, and the runbook our consultants use, all in the fixed monthly fee.

If you run a busy WordPress site and you are not using WP-CLI yet, the highest-leverage move is to install it on the server, get wp plugin list working from a cron, and start logging weekly state to a folder. The full setup takes about an hour. The payback shows up at the next emergency.

Start here

Hit this one
yourself?

If any of the above is happening on your stack, send us the symptoms. We triage the same day and quote before we start.

Which layer is on fire?
Your details stay with us. Always.