What Actually Goes Wrong When a Client Asks for “AI” in the Brief
Almost every brief grows the word "AI" somewhere in it, and hardly any of them say what that means. These…
Every WordPress support forum gives the same first answer to “my site broke after I installed a plugin”: deactivate everything, then reactivate plugins one at a time until it breaks again. It’s not wrong, exactly. It’s just slow, and on a client site running forty-plus active plugins with a WooCommerce catalog behind it, slow turns into a few hours of billable time spent re-clicking checkboxes in wp-admin. We don’t run that process first anymore. Here’s the actual order of operations we work through when a plugin conflict lands in the queue, with the WP-CLI commands and debug-log habits attached to each step, not just the theory behind them.
Before deactivating a single plugin, get an exact record of what was running when the bug happened — not “I think it was the SEO plugin and maybe the page builder,” an actual list with versions attached. The fix six months from now depends on knowing precisely which combination broke and which one didn’t.
wp plugin list --fields=name,status,version --format=csv > snapshot.csv
wp theme list --fields=name,status,version
wp cli info
wp cli info also hands you the PHP version, which matters more than people expect. We’ve closed out a “plugin conflict” ticket that turned out to be a plugin that simply didn’t support PHP 8.1 yet — no second plugin involved at all.
Never binary-search on the live site. Clone it — most hosts worth using give you one-click staging; if not, a wp db export and a duplicate install takes ten minutes. On the clone, turn every debug constant on:
wp config set WP_DEBUG true --raw
wp config set WP_DEBUG_LOG true --raw
wp config set WP_DEBUG_DISPLAY false --raw
wp config set SCRIPT_DEBUG true --raw
SCRIPT_DEBUG is the one people forget, and it’s the one that matters for JS conflicts. It loads the unminified .js files instead of the bundled .min.js, so a browser console error points at an actual line in an actual file instead of plugin.min.js:1.
If the conflict is a hard fatal and wp-admin won’t load at all, the instinct is to FTP in and start renaming plugin folders. That works — WordPress deactivates any plugin whose folder disappears — but it’s blunt, and it’s slow over SFTP on a cheap host. Try this first:
wp plugin deactivate suspect-plugin-slug --skip-plugins
--skip-plugins is a global WP-CLI flag that boots WordPress without loading any plugins, so the fatal never fires during bootstrap and the deactivate command actually gets to run. It’s the most useful flag nobody mentions in “how to fix a white screen” articles. Keep the folder-rename trick in your back pocket for the rare host that blocks WP-CLI entirely, but check for CLI or SSH access before you touch FTP.
On the reproducible staging copy, deactivate everything and bring plugins back in halves instead of individually:
wp plugin deactivate --all
wp plugin activate plugin-a plugin-b plugin-c plugin-d plugin-e
Reproduce the bug. If it shows up, the fault lives in that half; if not, it’s in the other half. On a site with forty active plugins that’s six rounds, not forty. Here’s where we actually disagree with the standard advice: one-at-a-time reactivation only ever proves that a single plugin, alone, causes the bug. A good share of real conflicts are pairwise — two plugins that are each fine on their own and only break together, usually because they’re hooking the same filter, registering the same script handle, or writing to the same custom table. One-at-a-time will never catch that, because neither plugin alone reproduces anything. Binary search does catch it, as long as you’re deliberate about splitting suspicious pairs into different halves on the next round instead of always cutting the list down the middle.
Sometimes deactivating either half “fixes” it, which usually means the bug depends on load order rather than on which plugins are simply active — two plugins hooking the same action at the same priority, and whichever loads second wins. Dump the actual callback list for that hook instead of guessing:
wp eval 'global $wp_filter; print_r( $wp_filter["template_redirect"]->callbacks );'
That prints every callback registered on template_redirect, grouped by priority, with the exact class or function name attached. It turns “these two plugins don’t get along” into “Plugin B unhooks a canonical redirect that Plugin A depends on, both at priority 10, and B happens to load second” — a bug you can patch with a one-line priority change in a small must-use plugin, instead of a support ticket asking two vendors to make up.
wp-content/debug.log is the first place to check, and grepping it beats scrolling it:
grep -i "fatal error" wp-content/debug.log | tail -20
The gap people hit: a fatal that happens while a plugin file is loading, before WordPress finishes bootstrapping and evaluates your WP_DEBUG_LOG constant, sometimes never reaches debug.log at all. It goes to the server’s own PHP error log instead. If debug.log is suspiciously quiet during a known white screen, that log is next, and it’s worth running php -l path/to/plugin-file.php to rule out a plain syntax error before assuming you’re dealing with a runtime conflict.
Plenty of “plugin conflicts” are pure JavaScript — two plugins registering the same script handle at different versions, or each loading its own copy of Select2 or Slick with jQuery in a different mode. A PHP fatal log won’t show this; the browser console and an enqueue dump will:
wp eval 'global $wp_scripts; foreach ( $wp_scripts->queue as $h ) { echo $h . " => " . $wp_scripts->registered[$h]->src . PHP_EOL; }'
If the same handle name is registered from two different plugin directories, or the same library loads twice under two different handles, that’s the conflict — and the fix is usually one wp_deregister_script call in a small mu-plugin, not the removal of either plugin.
We’ve watched a client’s dev spend an afternoon deactivating plugins for a bug that was sitting in the theme’s functions.php the whole time. Before going further down the plugin list, switch to a default theme and see if the symptom survives:
wp theme activate twentytwentyfour
If the bug disappears, no amount of plugin deactivation was ever going to find it. The fault is in the child theme, not in any plugin, and everything above needs to point at theme code instead.
Once things are narrowed down this far, Query Monitor is worth installing — it shows hook execution, HTTP API calls, and database queries per request without hand-rolling wp eval dumps for everything. Our actual opinion: pull it back off once you have your answer. A debugging plugin left active “just in case” on a production site is exactly the kind of unaccounted-for plugin that kicks off the next conflict investigation.
Once you’ve found it, one line in a shared doc — plugin X plus plugin Y plus theme Z on PHP 8.2 produces this symptom — saves the next person, possibly future you, from re-running this entire process on the same combination six months from now.
To be honest about it: this whole workflow is overkill for a three-plugin hobby blog. On a site that small, just deactivate them one at a time — it’ll take five minutes and binary search buys you nothing. The staging clones, the hook dumps, the --skip-plugins trick — those earn their keep specifically on the client sites where guessing costs real hours: the ones running thirty, forty, sixty active plugins where nobody fully remembers what half of them are for anymore. That’s most of the WordPress and WooCommerce builds we get called in on, which is exactly why this list exists as a checklist and not a one-off blog post we wrote and forgot about.
Tell us what you're building and we'll tell you honestly whether we're a fit.
Almost every brief grows the word "AI" somewhere in it, and hardly any of them say what that means. These…
Self-hosted n8n is easy to stand up and easy to neglect once it's quietly running a client's order pipeline. Here's…
Checkout.liquid died in two waves — August 2024 for the core checkout, August 2025 for Thank You and Order Status…