| name | wp2shell-cleanup |
| description | Use after wp2shell-audit has confirmed a Pantheon WordPress site is compromised by the wp2shell vulnerability chain (CVE-2026-60137 / CVE-2026-63030) — guides remediation (upstream update on a multidev, fake-admin removal, optional password reset, salt rotation, DB cleanup) via scripts/wp2shell-cleanup.sh, in order, with a human decision point before every destructive or ambiguous action. |
wp2shell Cleanup
Overview
Remediation companion to wp2shell-audit (a separate repo/skill). Audit first, confirm compromise, then use this. Every action in scripts/wp2shell-cleanup.sh defaults to preview-only and requires its own explicit flag plus --execute to actually run — there is no single command that does everything, by design.
Test against a seeded local DDEV or Lando copy, or a multidev, before running anything for real. Never run the data-remediation steps (fake-admin removal, password reset, salt rotation, DB cleanup) against Dev, Test, or Live first: clone the affected environment into a multidev, run those steps there, confirm the site loads correctly, only then repeat them against the real environments. The upstream-update step is different — it's a code change, not a data change, so it does not get repeated per-environment; see Process below. Read docs/remediation-guide.md in full before running anything with --execute.
Always confirm with the human before running any --execute action — including against a multidev, DDEV, or Lando project they've described as disposable or test-only. Prior "this is just a test site" framing does not carry forward as standing permission for every subsequent destructive action.
Install
Clone or copy this repo into ~/.claude/skills/wp2shell-cleanup/ (personal) or <project>/.claude/skills/wp2shell-cleanup/ (project-scoped). You can also run scripts/wp2shell-cleanup.sh directly without Claude — see the README.
Prerequisites
terminus CLI installed and authenticated, with access to the target site.
- Confirmed compromise from a
wp2shell-audit run — this skill assumes that already happened, it does not re-detect anything itself.
ddev or lando instead, if testing --clean-db/--delete-user/--reset-passwords locally rather than running against a real site — see Notes below.
Process
Follow docs/remediation-guide.md's order of operations. As the agent running this:
- Confirm compromise is real before starting — ask for or re-run the audit if you don't have confirmed findings in hand (specific flagged post IDs, user IDs, or log lines). Don't proceed on a vague "the customer thinks they were hacked."
- Apply the upstream update once, on its own dedicated multidev (
--apply-upstream --execute) — this is a code change, not a data change, so it is never repeated per-environment. After verifying the patch on that multidev, merging it to Dev and deploying through Test → Live is a separate, manual step this skill does not do — never run --apply-upstream directly against Test or Live, it isn't the supported path (they're read-only outside upload paths in Git mode).
- Test the remaining steps (4–7 below) on a multidev cloned from the compromised environment first. Confirm the site loads and behaves correctly, then repeat those steps independently against each real environment your audit actually found forged content on, in order: Dev, then Test, then Live. Unlike the upstream update, these are data operations — each environment has its own separate database, so they genuinely need to run once per environment.
- For fake-admin removal, stop and get explicit confirmation from the human you're working with before deleting anything. This requires judgment this skill cannot make on its own — surface what was found, wait for confirmation, then act. Remove confirmed-fake admins (
--delete-user ID --execute) before Step 5 (if used) and Step 6 — a still-valid account can just log back in otherwise. On a WordPress Multisite (WPMS) network, add --multisite here too — without it, a confirmed-fake account whose role lives only on a subsite (not the main site) fails to delete outright ("no roles found"), not just partially; --multisite removes the account from the whole network, not just one site.
- (Optional) Reset every remaining user's password (
--reset-passwords --execute) after fake admins are removed. Ask whether the human wants this step at all — it's optional specifically because how you communicate a forced reset to users is a process decision outside this skill's control, not something to default into. Add --multisite here too on a WPMS network — without it, a user whose only role is on a subsite is invisible to this action entirely, so their password silently never gets reset even though the run reports success.
- Rotate salts (
--rotate-salts --execute) after fake admins are removed (and passwords reset, if used), to invalidate any other stolen session or credential.
- DB cleanup (
--clean-db --execute) is lower-risk once compromise is confirmed — same dry-run-then---execute pattern, but doesn't require the same live back-and-forth as account deletion. Always run the preview first (no --execute) and read the post_status breakdown it prints before adding --execute. The invalid-post_status check whitelists WordPress core's and WooCommerce's own standard statuses, but any other plugin's non-standard status still shows up as a candidate — the preview prints exactly which statuses those are alongside the site's active plugin list. Match each one to a real, installed plugin in that list yourself; don't judge by the name alone. For anything confirmed legitimate, re-run with --exclude-status STATUS (repeatable) added before ever adding --execute. If the site is a WordPress Multisite (WPMS) network, add --multisite — without it, only the main site gets checked/cleaned, and a compromised subsite's forged content is silently skipped, not just under-cleaned. Ask if you don't already know whether it's multisite, or take the user's word for it if they say so directly. Add --include-oembed-cache too if you want paired oembed_cache rows (modified within 5s of a confirmed forged changeset) removed alongside it — off by default, since oembed_cache is ordinary cache data on its own.
Notes
- Every destructive script action backs up what it's about to touch (as JSON/TSV in
./wp2shell-cleanup-backups/) before doing anything, and defaults to preview-only — except --reset-passwords, which deliberately creates no backup file (the new password is never stored, logged, or shown, since nobody needs it — affected users use WordPress's own "Lost your password?" flow).
--rotate-salts and --apply-upstream require --site SITE.ENV specifically — they're Pantheon platform-level Terminus commands, not WP-CLI passthroughs, so --wp alone isn't enough for those two actions.
--apply-upstream applies to whatever --site points at directly — that should be a multidev created for this purpose, not Live/Test/Dev. Creating the multidev, and merging/promoting afterward, are manual steps this skill does not do for you.
- Both Terminus platform-level actions (
--rotate-salts, --apply-upstream) pass --yes to their underlying terminus command — without it, a non-interactive run silently declines Terminus's own confirmation prompt and does nothing, while still printing a success message.
--ddev (in place of --site/--wp) sets the WP-CLI backend to ddev wp, for testing every action against a local DDEV copy before running for real. Without --site, --rotate-salts and --apply-upstream fall back to plain WP-CLI equivalents (wp config shuffle-salts, wp core update) — real local tests, but not the same mechanism as the Terminus platform-level actions --site uses.
--ddev-path PATH (optional, requires --ddev) tells the script to cd into PATH before running any ddev command, since DDEV resolves which project a command targets from the current working directory, not a project name/path argument. Only needed if you're not already running this script from inside the DDEV project's own directory. Errors if PATH has no .ddev directory.
--lando/--lando-path PATH are the same idea, for a Lando project instead — sets the WP-CLI backend to lando wp, same fallback/path-resolution semantics as --ddev/--ddev-path. Errors if PATH has no .lando.yml. Cannot be combined with --ddev — pick one local backend.