PlaybooksPlugin Audit & Cleanup

Plugin Audit & Cleanup

Plugin Audit & Cleanup

A documented playbook for plugin audit and cleanup work on a client WordPress site. This is hourly technical support work, not a productized engagement — the playbook exists because the process is predictable enough to be worth standardizing across team members and clients.

When this work comes up

Triggers vary but the most common are:

  • A host (WP Engine, Kinsta, Cloudways) flags performance issues and is upselling hardware/plan upgrades
  • A care plan client is experiencing slowdowns, 504 timeouts, or database bottlenecks
  • A new client is being onboarded and has obvious plugin bloat from years of accumulation
  • An hourly support ticket reveals plugin conflicts or unused plugins consuming resources
  • A site audit (for any reason) surfaces 30+ active plugins with no clear ownership

The agency owner makes the call on whether to run this playbook. The developer never starts it unilaterally based on what they see during other work — surface findings to the owner and let them decide whether it becomes a scoped engagement.

What the work does

Five things, in this order:

  1. Inventories every plugin currently active on the site and determines which are genuinely in use
  2. Safely removes plugins that aren't needed, in batches with observation windows
  3. Resolves any specific high-impact plugins identified during investigation (heavy tracking plugins, oversized log tables, abandoned analytics, etc.) by either removing them outright or replacing them with lighter alternatives
  4. Cleans up the database residue left behind by current and previously-removed plugins (orphan tables, options, postmeta, cron jobs)
  5. Hands the agency owner an engagement summary capturing what changed and what's recommended next

It is not a full performance optimization. We're not refactoring themes, tuning caching layers beyond obvious misconfiguration, or redesigning information architecture. Performance improvements happen as a side effect of removing bloat, but they're not the headline.

It is not open-ended. We work within the scope the owner quoted the client, surface scope creep, and stop at the engagement boundary.

Who does what

RoleResponsibilities
Agency ownerSells the engagement, handles all client communication, makes judgment calls on architectural decisions, reviews and signs off on anything affecting scope or timeline.
DeveloperRuns the engagement day-to-day. Owns investigation, staging testing, production execution, and database cleanup. Maintains the engagement state document. Escalates when the playbook says to.
Claude project assistantPair-works with the developer throughout. Suggests commands, interprets output, enforces methodology, catches mistakes. Does not talk to clients.

Tools & AI assistance

Required tools

  • SSH access to the host (WP Engine, Kinsta, Cloudways, generic VPS, etc.)
  • WP CLI — primary investigation and execution tool
  • Host control panel access for snapshots and staging environment creation
  • Code editor for inspecting theme files and writing any replacement code
  • The Claude project for guidance throughout (see below)

Installed on staging during engagement

  • Query Monitor — request-level performance and query analysis
  • WP Crontrol — cron job inspection
  • WP Advanced Database Cleaner Pro — orphan detection and removal (Phase 6 only; PixelPress holds a Pro license)

Reference tools

  • GTmetrix or PageSpeed Insights — before/after performance baseline
  • Browser developer tools — visual inspection during staging testing

The Claude project

The team uses a dedicated Claude project for this work:

The project is configured with a system prompt that establishes a senior-engineer-mentoring-junior dynamic, enforces the methodology described in this playbook, refuses shortcuts, and provides WP CLI commands and SQL queries throughout. The system prompt is maintained inside the project itself — if it needs updating, the agency owner edits it directly there.

Session pattern:

  1. Developer opens a new chat in the project at the start of work
  2. Developer states which engagement they're on, which phase, and pastes the current engagement state document
  3. Claude reviews the state, confirms the plan for the session, asks any pre-flight questions
  4. Claude walks the developer through the next batch of work command by command, interpreting output as the developer pastes it back
  5. At end of session, Claude produces a state doc update snippet for the developer to paste into the master state doc
  6. Session ends

Don't try to run an engagement without the Claude project — the methodology assumes that interaction pattern.

When SSH isn't available (~20% of engagements)

Fall back to admin-level WordPress access + phpMyAdmin or equivalent DB access + FTP/SFTP for file inspection. The methodology still works but is slower. If a client can't provide SSH and the engagement involves more than ~30 active plugins, raise it with the owner before starting — the time estimate may need adjustment.

The engagement state document

Every engagement has a state document. It's a Google Doc the developer maintains throughout. It's the single source of truth for engagement progress and it's what gets pasted at the start of each Claude session.

See Engagement State Template for the structure. Copy it at the start of every engagement.

The developer updates this document after every session. The Claude assistant produces a markdown update snippet at the end of each session.

The seven phases

Phase 0 — Pre-flight

Mechanical, boring, critical. Before any technical work starts.

Checklist:

  • Engagement brief received from owner (scope, hours, client constraints, deadlines)
  • Access verified: SSH credentials work, host portal access works, wp --info runs successfully
  • Backup point confirmed fresh (host snapshot within last 24 hours, or take one now)
  • Staging environment created or refreshed from current production
  • Engagement state document created and populated with intake info
  • Known-good plugin list from client documented in state doc
  • Any client-specific constraints noted (page builder constraints, plugins not to touch, microsites or special templates to be careful around)
  • Phase 0 marked complete in state doc

Do not proceed to Phase 1 until every item is checked. If any item can't be checked, escalate to the owner.

Phase 1 — Production reconnaissance (read-only)

Build a complete picture of current state without changing anything. All commands read-only. We're on production.

Run these in order, capturing output to a working notes file or pasting into the Claude session as you go.

Plugin inventory

wp plugin list --format=csv > plugins-inventory.csv
wp plugin list --status=active --format=count
wp plugin list --status=inactive --format=count

Theme inventory

wp theme list --format=csv
wp option get template
wp option get stylesheet

Autoloaded options analysis (often where the silent killer lives)

wp option list --autoload=yes --format=csv > autoload-options.csv
wp db query "SELECT option_name, LENGTH(option_value) AS size FROM wp_options WHERE autoload='yes' ORDER BY size DESC LIMIT 30"

Database table sizes

wp db query "SELECT table_name AS 'Table', ROUND(((data_length + index_length) / 1024 / 1024), 2) AS 'Size (MB)', table_rows AS 'Rows' FROM information_schema.tables WHERE table_schema = DATABASE() ORDER BY (data_length + index_length) DESC"

Cron job inventory

wp cron event list --format=csv > cron-events.csv

Content scale by post type

wp post list --post_type=any --format=count
wp post list --post_type=post --format=count
wp post list --post_type=page --format=count
# Plus any custom post types discovered

Multisite check

wp core is-installed --network
# If true, engagement is more complex — escalate before proceeding

WordPress and PHP version

wp core version
wp eval 'echo PHP_VERSION;'

By end of Phase 1, the state doc should have:

  • Plugin counts (active/inactive)
  • Top 10 autoloaded options by size
  • Top 10 database tables by size (this is where heavy tracking or logging plugins surface as outliers)
  • Total cron event count and any obviously orphaned ones
  • Content scale by post type
  • WP and PHP versions

Typical time: 1-2 hours including documentation.

Phase 2 — Plugin investigation

Work through unknown plugins one by one. Output is the tier classification in the state doc.

For each plugin not on the known-good list, investigate:

Theme code references

# From wp-content/themes:
grep -r "plugin_function_name\|plugin_class_name\|plugin_shortcode" .

Each plugin has identifiable function names, class names, or shortcodes. Check the plugin's main file for add_shortcode, add_action, exported functions, and search the theme for those references.

Shortcode usage in content

wp db query "SELECT ID, post_title, post_type FROM wp_posts WHERE post_content LIKE '%[shortcode_name%' AND post_status='publish'"

Run for each shortcode the plugin exposes.

Block usage (for Gutenberg sites)

wp db query "SELECT ID, post_title FROM wp_posts WHERE post_content LIKE '%wp:plugin-namespace%' AND post_status='publish'"

Widget usage

wp option get sidebars_widgets
wp option get widget_plugin-widget-name

Cron events owned by the plugin

wp cron event list | grep plugin_keyword

Database footprint

wp db tables | grep plugin_prefix
wp option list | grep plugin_prefix

Once investigation is complete for a plugin, classify into one of four tiers:

TierDescriptionAction
A — Safe to removeZero theme references, zero shortcode/block usage in content, no critical cron jobs, no dependencies. Examples: WP All Import after one-time migration, abandoned analytics plugins, leftover campaign plugins.Stage Batch 1
B — Likely safe, verifyMinor references that look vestigial, shortcode usage only in old drafts/trash, cron jobs that haven't fired meaningfully in months.Stage Batch 2
C — High-risk unknownFound references but unclear if functional, custom plugins with no documentation, plugins tied to microsites or special templates, plugins integrating with external systems (CRMs, analytics, payment processors).Escalate to owner for decision before action
D — KeepConfirmed active dependencies.Document where it's used; don't re-investigate next time

Typical time: 3-5 hours depending on plugin count.

Phase 3 — Staging testing

All testing happens on staging. Production is not touched in this phase.

Refresh staging from production first. Host's copy environment feature does this — confirm it's recent before proceeding.

Install testing tools on staging only:

  • Query Monitor
  • WP Crontrol

Deactivate plugins in batches. Don't deactivate everything at once — you won't know which deactivation caused which symptom.

  • Batch 1: All Tier A plugins (lowest risk, highest confidence)
  • Batch 2: Tier B plugins, in groups of 3-5
  • Batch 3: Any Tier C plugins the owner has cleared for staging testing

After each batch, walk through:

  • Homepage
  • Main blog/news index
  • A representative single post or article
  • Donation/conversion pages (if applicable)
  • Contact page
  • Any microsite or special template pages flagged in engagement brief
  • WordPress admin dashboard
  • Common editor flows (creating a draft, editing media)
  • Multilingual variants if applicable
  • Mobile views via responsive mode

For each page, check:

  • Does it render correctly visually?
  • Does Query Monitor show new PHP errors or warnings?
  • Does form submission still work? (test with dummy data)
  • Are there broken images, missing scripts, or styling issues?

Document any breakages in the state doc. If a plugin's deactivation broke something, that plugin moves from Tier A/B to Tier D (keep) and we note why.

Typical time: 2-4 hours including testing.

Phase 4 — Production execution

Only plugins that passed staging testing get deactivated on production. Same batch structure as Phase 3.

Before each batch:

# Confirm we're on production
wp option get siteurl
# Take a host snapshot — usually manual via host portal

Then deactivate the batch:

wp plugin deactivate plugin-name-1 plugin-name-2 plugin-name-3

Spot-check critical pages immediately after. Same key pages as staging testing. Confirming changes behaved as predicted, not doing a full retest.

Document what was deactivated, when, and observed behavior.

Wait at least 24 hours before the next batch, ideally 48-72. This surfaces delayed issues — cron-dependent functionality that runs daily, background tasks, scheduled emails. If a problem appears, reactivate before continuing.

Do not delete plugins in the same session as deactivation. Deletion happens in Phase 6 or as a separate later task, after the full observation window has passed (default one week per batch, longer for any plugin with uncertain dependencies).

Typical time: 2-3 hours active work spread across 1-2 weeks calendar time.

Phase 5 — High-impact plugin resolution

Some plugins surface during Phase 1 and Phase 2 as outsized contributors to performance problems — a single plugin owning hundreds of MB of database tables, a tracking plugin with millions of rows, a logging plugin that hasn't been pruned in years, an analytics plugin doing heavy synchronous writes on every page request. These need their own resolution workflow because the answer isn't always "remove" — sometimes the client genuinely needs the functionality and a lighter replacement has to be designed.

This phase often runs in parallel with Phase 4 because it doesn't depend on the broader cleanup.

How to recognize a high-impact plugin during investigation:

  • Owns a database table in the top 5 by size or row count
  • Owns a heavy autoloaded option (>500KB)
  • Owns cron jobs that fire frequently and take significant time to complete
  • Generates slow query log entries
  • Was specifically flagged by the host or by the client as a performance concern

Common examples (not exhaustive — every site is different):

  • Heavy view/popularity tracking plugins (WordPress Popular Posts, Post Views Counter, similar)
  • Activity log plugins that have accumulated years of entries (WP Activity Log, Simple History, Stream)
  • Broken link checker–style plugins that scan and store findings indefinitely
  • Old backup plugins still writing logs to the database
  • Form plugins with massive entry tables (where the client may or may not still need the entries)
  • SEO plugins with extensive redirect logs or 404 logs (some Yoast features, RankMath)
  • Heavy analytics plugins logging synchronously to the database
  • Visitor tracking or heatmap plugins storing per-session data
  • Abandoned cart plugins that never prune old records

Investigation steps (adapt to the specific plugin):

# Identify the plugin's database tables and their sizes
wp db tables | grep plugin_prefix
wp db query "SELECT table_name, ROUND(((data_length + index_length) / 1024 / 1024), 2) AS 'Size (MB)', table_rows FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name LIKE '%plugin_prefix%'"

# Find where the plugin's output renders in theme code
grep -r "plugin_function_name\|plugin_class_name" wp-content/themes/

# Find shortcode usage in content
wp db query "SELECT ID, post_title FROM wp_posts WHERE post_content LIKE '%[shortcode_name%' AND post_status='publish'"

# Find widget/sidebar usage
wp option get sidebars_widgets | grep -i plugin_keyword

# Check for block usage (Gutenberg sites)
wp db query "SELECT ID, post_title FROM wp_posts WHERE post_content LIKE '%wp:plugin-namespace%' AND post_status='publish'"

Worked example: WordPress Popular Posts

WPP is the most common case the team encounters and a useful concrete reference. It's a popularity tracking plugin that logs every pageview into wp_popularpostssummary and wp_popularpostsdata, neither of which gets pruned by default. On long-running sites, these tables routinely hit millions of rows and cause minute-long queries during nightly backups.

# Check the damage
wp db query "SELECT COUNT(*) FROM wp_popularpostssummary"
wp db query "SELECT COUNT(*) FROM wp_popularpostsdata"

# Find rendering
grep -r "wpp_get_mostpopular\|WordPressPopularPosts\|wpp-" wp-content/themes/
wp db query "SELECT ID, post_title FROM wp_posts WHERE post_content LIKE '%[wpp%' AND post_status='publish'"
wp option get sidebars_widgets | grep -i wpp

Decision tree — applies to any high-impact plugin. Present findings to the owner, get direction:

If the plugin isn't rendering anywhere on the site: deactivate, truncate or drop its tables after backup, delete plugin. Developer executes with owner's signoff. This is the cleanest outcome and surprisingly common for plugins that were once used and then quietly replaced by something else without being uninstalled.

# After host snapshot, on staging first, then production after observation:
wp plugin deactivate plugin-slug
wp db query "TRUNCATE TABLE wp_plugin_table_1"
wp db query "TRUNCATE TABLE wp_plugin_table_2"
# Or DROP TABLE if removing the plugin entirely and we're confident

If the plugin is rendering something the client needs: stop. This becomes a senior decision. Replacement strategies vary by plugin type — editorial curation via ACF, precomputed values stored in post meta, offloading to a third-party service, a lighter alternative plugin, custom theme code. Owner makes the architectural call and either takes the work or assigns it back to the developer with specific direction.

Typical time:

  • Removal path (plugin not rendering): 1-2 hours
  • Replacement path (custom code or migration to lighter alternative): 4-8+ hours depending on complexity

Phase 6 — Database cleanup

Run only after plugins from Phase 4 have been deleted (not just deactivated) and observation windows have passed. Running it during the observation period will destroy data for plugins we're still watching.

Install WP Advanced Database Cleaner Pro on staging first.

Run discovery only — no cleanup yet.

The plugin identifies:

  • Orphan database tables (left by previously-removed plugins)
  • Orphan options in wp_options
  • Orphan postmeta and usermeta
  • Orphan cron events
  • Autoload candidates (options currently autoloaded that probably shouldn't be)

Review every category before any removal:

For orphan tables — some "orphan" tables are custom (built by a previous developer outside any plugin), not orphans. Every flagged table needs a sanity check: does the table name pattern match a plugin we removed? Is there evidence the table was custom-built? When in doubt, leave it alone.

For autoload changes — only flip autoload to "no" on options that are clearly orphan or clearly oversized (a 1MB+ option autoloaded on every request is almost always wrong). Mass-flipping autoload can break plugins that legitimately need their settings loaded early.

Execute cleanup on staging. Test the site afterward — homepage, key flows, admin. Confirm nothing broke.

Take production snapshot, execute on production.

Document:

  • Tables removed (count, total size)
  • Options removed (count)
  • Postmeta/usermeta removed (count)
  • Cron events removed (count)
  • Database size before and after

These numbers go in the engagement summary.

Typical time: 2-3 hours.

Phase 7 — Wrap-up

Generate the engagement summary using the Client Summary Template. Update the state doc to reflect completion. Document any recommendations for follow-on work.

Recommendations typically include things noticed but not addressed:

  • Page builder bloat (Divi, Elementor, etc.)
  • Theme inefficiencies surfaced during investigation
  • Caching configuration issues
  • Image optimization opportunities
  • Plugins kept but with better alternatives available
  • Future plugin governance suggestions

These become potential follow-on work for the owner to discuss with the client.

Typical time: 1 hour.

Total engagement hours

Realistic estimate for a typical mid-complexity engagement (35-50 active plugins, single WordPress install, no multisite, accessible host):

PhaseHours
0 — Pre-flight1
1 — Production recon1-2
2 — Plugin investigation3-5
3 — Staging testing2-4
4 — Production execution2-3
5 — High-impact plugin resolution (removal path)1-2
5 — High-impact plugin resolution (replacement path, if needed)+4-8
6 — Database cleanup2-3
7 — Wrap-up1
Total (removal path)13-21 hours

Engagements with multisite, very high plugin counts (60+), or complex microsite architectures can run 25-35 hours. Engagements without SSH access add 25-50% to investigation phases.

Hard rules

These never bend, regardless of time pressure or client requests:

  1. Staging first, always. Any change touches staging first. Production changes only after staging is tested and observed.
  2. Deactivate before delete. Plugins get deactivated, observed for the agreed window (default one week), then deleted. Never delete in the same session as deactivation.
  3. Snapshot before production changes. Every batch of production changes is preceded by a fresh host snapshot. No exceptions.
  4. Read-only first. Phase 1 is entirely read-only. No deactivations, no destructive queries, no file edits.
  5. No destructive SQL without explicit confirmation. Any DROP, DELETE, TRUNCATE, ALTER requires confirming environment, fresh backup, and exactly what will be affected — stated out loud, every time.
  6. Don't touch what you can't identify. Unclear plugins stay. The cost of keeping a plugin is small; the cost of breaking client functionality is enormous.
  7. No client communication without explicit instruction. Developer doesn't email clients. Escalations go to the owner.

Common gotchas

The "orphan" trap. A plugin that's been deactivated but not deleted will have its data flagged as orphan by Advanced Database Cleaner. Never run cleanup operations during the observation period.

WPML and translation tables. Sites with WPML have additional plugin-prefixed tables (wp_icl_*) that should generally be left alone. Don't classify them as orphans.

ACF custom tables. If the client uses ACF Pro with custom database tables configured for field storage, those tables won't be tied to a recognizable plugin signature. Don't classify them as orphans.

Custom mu-plugins. Plugins in wp-content/mu-plugins/ are must-use plugins. Always active, often custom-built, frequently undocumented. Investigate carefully and surface findings to the owner rather than treating them like standard plugins.

Multilingual sites with separate content. On WPML or Polylang sites, plugins that render content (related posts, popular posts, taxonomies, etc.) may behave differently per language. Test all language variants during staging testing.

Cache plugins that don't get along. Multiple caching layers (host cache + WP Rocket + CDN) can mean cache doesn't invalidate immediately after changes. Always purge cache manually after changes during testing.

Form plugins with stored entry data. Gravity Forms, Ninja Forms, and similar store entries in their own tables. Never deactivate a form plugin without checking whether there's stored entry data the client may still need.

Cron-dependent functionality firing daily. Some integrations sync once a day, send digest emails on schedule, or do background maintenance overnight. Issues from deactivating a plugin with this behavior may not show up for 24+ hours. This is why we wait between batches.

Wordfence and other security plugins. When the host has its own WAF (like WP Engine) and the PixelPress care plan provides WAF coverage, redundant security plugins may be safe to remove. Always confirm with the owner — security removal is high-stakes and there may be specific configurations the client relies on.

Host-specific notes

WP Engine

  • Snapshots are taken via the User Portal → environment → Backups. Wait for confirmation before proceeding.
  • Staging copies are made via the User Portal → environment → "Copy environment". Refreshing staging from production overwrites the staging database and files entirely.
  • SSH access uses the format environment@environment.ssh.wpengine.net on port 22.
  • WP CLI is installed and available on the WP Engine SSH gateway.
  • WP Engine has its own object cache and page cache — clear via User Portal → environment → Caching after major changes.
  • WP Engine has a built-in WAF (Global Edge Security on most plans). If the site also has Wordfence active, this is often redundant.
  • WP Engine has a list of disallowed plugins — confirm any replacement plugins aren't on it before installing.

(Other host appendices to be added as needed.)

Escalation triggers — when to stop and ping the owner

  • Engagement brief is unclear or missing information needed to proceed
  • Site is on multisite and that wasn't expected
  • Evidence of malware, hacking, or serious misconfiguration outside engagement scope
  • Custom mu-plugin or custom theme code that does something unfamiliar
  • Plugin appears load-bearing for client business logic (custom checkout/donation/booking flows, payment processor integration, CRM sync, etc.)
  • Production behavior differs from staging predictions
  • Error during production change that doesn't resolve in 30 minutes
  • Client contacts the developer directly
  • Engagement appears to be growing beyond scope
  • About to make any architectural decision (replacement plugin choice, custom code design)
  • Phase 5 (high-impact plugin resolution) — escalate after investigation, before execution, every time

When escalating, write a clear summary for the owner: situation, options, recommendation. The Claude project assistant will help frame the escalation.