When the cleanup went wrong

The malware cleanup broke your site. Here is how to get it back.

Most cleanup tools take the whole file when a few injected lines were the problem. If that file belonged to a working plugin or theme, WordPress still calls it, and what breaks depends on whether the tool deleted the file, emptied it, or renamed it. Those three look nothing alike, so start by working out which one you have.

Checked against a live WordPress install on .

Find the file the error names

If the site returns a fatal error, the error already contains the answer. WordPress hides it from visitors by default, so your first job is making it visible to you.

  1. Switch the log on. Open wp-config.php and set WP_DEBUG to true, WP_DEBUG_LOG to true and WP_DEBUG_DISPLAY to false, above the line that says to stop editing. With display off, visitors keep seeing WordPress's error page while every line still lands in the log.
  2. Read the log. It is written to wp-content/debug.log. WP_DEBUG_LOG also takes a path string if you would rather put it somewhere the public cannot reach.
  3. Reload the broken page once, then read the last entry. The fatal names the file that failed and the line that tried to use it. That path is the file you are going to restore.
  4. If the log stays empty, check the host's error log. Most hosting panels expose it under a Logs or Errors section. A fatal that kills PHP before WordPress configures its own logging shows up there and nowhere else.

A deleted plugin file produces a recognisable trace. The first frame sits in wp-settings.php, in the loop that includes every active plugin, and the message carries the plugin's own path. That tells you the damage is in wp-content/plugins rather than in core, before you look at anything else.

Leave WP_DEBUG_DISPLAY at false for the whole of this. A fatal printed to the page leaks absolute paths, and on a site that was just cleaned those paths are worth something to whoever wrote the injection.

Deleted, emptied, or renamed

Cleanup tools take a file in three different ways, and each one produces a completely different symptom. The table comes from breaking a WordPress 7.0.4 install one way at a time and watching what happened.

What the cleanup did What a visitor sees What the log says What the admin says
The file was deleted and a plugin still requires it HTTP 500 and the critical error page A fatal error naming the missing file, raised inside the plugin include loop Down with the same error
The plugin's main file was renamed or moved HTTP 200, the site renders Nothing Silent until you open the Plugins screen, which deactivates the plugin and says the plugin file does not exist
The plugin's main file was emptied to zero bytes HTTP 200, the site renders Nothing The plugin disappears from the list while staying active in the database

The emptied file is the one that fools people

A truncating cleanup leaves the file exactly where it was, at zero bytes. WordPress serves HTTP 200, the page renders, and debug.log receives nothing. The plugin stays in the active_plugins option in the database. But the function WordPress uses to read plugin headers returns nothing for an empty file, so the plugin vanishes from the Plugins screen and from wp plugin list at the same time. The feature stops working and no error is raised anywhere.

That makes wp plugin list an unreliable inventory after a cleanup, which matters because it is the first thing most people run. Cross-check it two ways. Read the option directly with wp option get active_plugins, or with get_option('active_plugins') if you are already in PHP, and list the directory with a plain ls -l wp-content/plugins. A plugin present in the option, present on disk, and missing from the list has an empty or unreadable main file.

The renamed file fixes itself, once

A missing main file also produces HTTP 200 and no log line, and WordPress leaves the plugin active until somebody opens the Plugins screen. That screen is the only place in WordPress that validates the active list. When it runs, it drops the entry and prints that the plugin has been deactivated due to an error, and that the plugin file does not exist. So opening the Plugins screen both fixes this case and tells you which plugin it was.

An emptied file gets no such treatment. WordPress never validates it, because as far as the validator is concerned the file is right there.

Put that one file back

Restore the single file the error named, from the upstream copy of the exact version you have installed. Copying one file out of an archive changes nothing else on the site, which is what you want while you still do not know how far the damage goes.

A core file

Download https://wordpress.org/wordpress-<version>-no-content.zip for the version you run, unzip it somewhere outside the site, and copy the one file across. That archive is the right one for this job: it carries no wp-content directory at all, so nothing you have can be overwritten by accident. For 7.0.4 it holds 3,901 files against the full archive's 4,401.

The full archive also restores the bundled default themes, Akismet and Hello Dolly into wp-content, so if you removed those on purpose they come back.

On some hosts, wp core download --force without --skip-content takes the tar.gz route, and PHP's archive reader cuts long paths at 100 bytes. On the install these tests ran on it happened twice in a row, and about 25 files under wp-includes/php-ai-client/ landed with truncated names while the real files were absent. Pass --skip-content, which forces the zip route, or unzip a manual download yourself.

A plugin or theme file

Plugins come from https://downloads.wordpress.org/plugin/<slug>.<version>.zip and themes from https://downloads.wordpress.org/theme/<slug>.<version>.zip. Match the version you had, not the newest. Read it off the Plugins or Themes screen if the admin loads, or out of the component's own readme.txt if it does not.

To see which versions exist, open https://wordpress.org/plugins/<slug>/advanced/ and look at the Previous Versions list. Themes have no such page, and that URL returns a 404 for them. Use the theme's directory listing at https://themes.svn.wordpress.org/<slug>/ instead, which shows one directory per released version.

Copy the file out of the zip by hand rather than reinstalling the component. wp plugin install --force and wp theme install --force delete the existing directory before unpacking, and they say so in their output. That is useful when a dropper is sitting inside the plugin folder, and expensive when the folder also holds a licence file, a config file or a cache the site needs.

A withdrawn version returns HTTP 404 with a nine-byte body reading "Not found", and it still sends a filename header, so a browser saves a file that looks like a successful download. Check the size before you unzip. A real plugin zip is not nine bytes.

A custom theme, a child theme, or a paid component

None of these has an upstream copy on WordPress.org. A child theme can be a single style.css with a parent named in its header, and reinstalling the parent never touches it. Trying to install a child theme by slug fails with a theme-not-found error, because it was never published anywhere. Paid plugins come from your vendor account, at the version you licensed. For everything else it is your host's backup, your version control, or your staging copy. The per-class procedure, including the paths you must never overwrite, is in restore modified core, plugin and theme files.

Check the restored file for the injection

You have just written the upstream copy over the file, so the injected lines that were in that file are gone with it. That settles one file. It settles nothing else, and the two most common mistakes at this point both come from assuming otherwise.

A re-download restores, it never removes. Forcing core back onto the site rewrites the files the archive contains and deletes nothing. In a test with five attacker-added files present, all five survived the run, and the install afterwards verified against checksums while still reporting five warnings about files that should not exist. A backdoor added next to core is untouched by every step on this page.

The verification commands are quieter than they look. Run wp core verify-checksums --include-root and read the warning lines, not the exit code. That command prints its success message before the warnings and exits 0 with unknown files sitting in wp-includes and wp-admin, which was captured live with four unknown files present, two of them named like droppers. Without --include-root it also misses unknown files at the web root.

Know what those commands do not cover before you trust a clean result. Core checksums throw away every wp-content entry before comparing, so they tell you nothing about your themes, plugins, uploads or wp-config.php. wp plugin verify-checksums skips any plugin with no WordPress.org manifest and still reports success, so premium and custom plugins are invisible to it, and it walks the files on disk rather than the manifest, so a deleted file is never noticed. There is no theme equivalent at all: wp theme verify-checksums is not a real subcommand and the theme checksum URL returns a 404. Much of the web says otherwise. A theme can only be checked by downloading the same version and diffing it.

If the file you restored was flagged in the first place, work out whether the detection was right before you decide the incident is over. Telling a real detection from a false flag covers that, and what each scanner does with a file it removes tells you whether your own bytes are still recoverable from the tool that took them.

When the broken file is an mu-plugin

Must-use plugins load from wp-content/mu-plugins before regular plugins, and there is no activation record for them. A fatal in one takes the whole site with it, admin included, and none of the usual escapes work.

This was tested rather than assumed. wp plugin list --status=must-use dies with the same fatal. wp plugin deactivate dies. So does wp option get siteurl --skip-plugins --skip-themes, because --skip-plugins does not skip must-use plugins. WP-CLI's own help says so.

The fix is on disk. Rename the file so it no longer ends in .php, for example from loader.php to loader.php.off. WordPress picks up only top-level .php files in that directory, so the renamed file stops loading and the site comes back. Then restore or repair it at your leisure and rename it back.

Two things make mu-plugins worth a second look after any cleanup. They have no upstream to compare against unless you wrote them, and wp plugin verify-checksums --all excludes them from its run entirely. Silence there means nothing was checked.

When the broken file is wp-config.php

Nothing can restore this file for you, and the reason is worth understanding before you waste an hour looking. No WordPress release archive contains a wp-config.php. Both the full 7.0.4 zip and the content-free one ship wp-config-sample.php and nothing else. The checksums API has no entry for it either, and WP-CLI names it explicitly in the exclusion list built into the tool, so a forced core download can neither restore it nor overwrite it. In one test its md5 was identical before and after.

Find it before you assume where it is. Run wp config path. WordPress allows wp-config.php to sit one directory above the WordPress root, and on the install these tests ran on it did exactly that: the config was at /var/www/html/wp-config.php while WordPress itself was in /var/www/html/wp/.

To rebuild it, take wp-config-sample.php from the release archive for the shape, then fill in your database name, user, password and host, your table prefix, and fresh authentication keys and salts. Changing the salts logs everybody out, which after a cleanup is a feature. Your host's control panel holds the database credentials, and your host's backup holds the original file if you would rather copy than rebuild.

Stop the next cleanup doing it again

One question separates a cleanup that can break a site from one that cannot: when the tool finds an injection inside a working plugin or theme file, does it write the file back without the injected lines, or does it take the file?

  • Ask before you let it act, not after. Across the popular scanners, deleting is the norm and keeping a copy is the exception. Every one of them was read from its shipped source on 18 August 2026, and the split is not what the marketing pages suggest.
  • Do not read Repair as restore. In every plugin that offers it, repair means downloading the vendor's published copy and writing it over what is there. Your edits to that file are gone either way, and the tool never reads your bytes first.
  • Assume custom code has no remedy at all. Every repair path in every one of these plugins is gated on recognising the file as WordPress core, a WordPress.org plugin, or a WordPress.org theme. A custom theme and a paid plugin match none of the three, so the only offer left on those files is removal.
  • Turn automatic removal off on a site with custom code. Review costs you a click. An unattended cleanup on a file you cannot download again costs you the file.

After any cleanup, check the result the way this page started. Load the front page and the admin, then compare the file the tool touched against the upstream copy of the same version. A file that now matches upstream byte for byte was restored. A file that is zero bytes was truncated, and you have the problem in the middle of this page.

Questions

The site is up but a feature stopped working and there is no error anywhere. What happened?
Look for a file of zero bytes. A plugin main file that exists but is empty produces nothing at all: HTTP 200, the page renders, and debug.log stays empty. WordPress still carries the plugin in the active list in the database, but the function that reads plugin headers returns nothing for an empty file, so the plugin drops off the Plugins screen and out of wp plugin list at the same moment. That is exactly what a cleanup that truncates a file leaves behind. List wp-content/plugins with sizes and look for a main file at 0 bytes.
wp plugin list no longer shows the plugin. Did the cleanup delete it?
Maybe, and that command cannot tell you. wp plugin list reads plugin headers, so a plugin whose main file was emptied is absent from the output while its directory is still on disk and its entry is still in the active_plugins option. A renamed or deleted main file drops out of the same list. Read the option and list the directory before you conclude anything, because those cases need different fixes and the list looks identical for all of them.
Can I re-download WordPress to undo the cleanup?
It restores files, and it removes none. A forced core download rewrites wp-admin, wp-includes and the root wp-*.php files from the archive, and it deletes nothing outside them. Measured across 9,124 files, one run repaired a tampered core file, added three 28-byte index.php placeholders under wp-content, and left every other wp-content file byte-identical. In the same test five attacker-added files survived untouched, and the install then verified against checksums while still printing five warnings about files that should not exist. So the re-download fixes core damage and does nothing about a backdoor sitting beside it.
WP-CLI dies with the same fatal no matter what I skip.
You are looking at a must-use plugin. The --skip-plugins flag does not skip mu-plugins, and WP-CLI says so in its own help. Every command dies, including wp option get with both skip flags set, because mu-plugins load before anything gets to decide not to load them. There is no activation record to switch off either. Rename the file on disk so it no longer ends in .php and the site comes back.
Where do I get a clean wp-config.php?
Nowhere public. No WordPress release archive contains one, only wp-config-sample.php, and the checksums API has no entry for it. WP-CLI names it in the exclusion list read from its own phar, so a forced core download cannot overwrite it, and the file's md5 was identical before and after such a run. No published copy of your database credentials exists to restore from. Your host's backup or your own copy is the only route. Run wp config path before you go looking, because wp-config.php is allowed to live one directory above the WordPress root and on the install these tests ran on it did.
I put the file back and the site loads. Is the site clean?
That one file is clean if you copied it out of the upstream archive for the version you have. The rest of the install is unmeasured. Core checksums cover wp-admin, wp-includes and the root files, and the check discards every wp-content entry before comparing, so they say nothing about your themes, plugins, uploads or wp-config.php. Plugin checksums skip any plugin with no WordPress.org manifest and still print Success, and they cannot see a deleted file at all. Themes have no checksum route in either WP-CLI or the API.

Next

Cleanup that leaves the file working

Every failure on this page starts the same way. A tool took a whole file when a few lines were the problem. Segurium removes the injected lines and writes the cleaned file back to the same path, so the plugin keeps loading and the theme keeps the function it was called for. It writes an empty body only for a file that is nothing but malware, it stops and leaves the file untouched when it cannot produce a clean version, and it takes an encrypted copy of the original on your own server before it writes anything.