It belongs to something you can name
A host’s file usually carries the host’s name in its header or folder, and the host’s documentation or support can confirm it. Ask before removing anything you cannot place.
Guide · Must-use plugin loaders
Must-use plugins are PHP files that WordPress loads from wp-content/mu-plugins on every request, before ordinary plugins, with no way to deactivate them from the admin area. Hosts and agencies use them legitimately. An attacker uses them because a file there survives a plugin cleanup, does not appear in the normal plugin list, and can hide itself — or a rogue administrator — from the screens you would check. This guide covers how to read the folder directly, tell a host’s files from a loader, and remove one in an order that holds.
Published Last reviewed
How must-use plugins work
WordPress reads the must-use plugin directory — wp-content/mu-plugins unless wp-config.php moves it with WPMU_PLUGIN_DIR — and loads every PHP file directly inside it, in alphabetical order, on every request. There is no activation and no deactivation: if the file is there, it runs. Must-use plugins load after the drop-ins and before every ordinary plugin and the theme.
Only files directly in the folder load. A PHP file in a subfolder of mu-plugins runs only if a file at the top level includes it — which is exactly how legitimate must-use setups load larger code, and how a loader can keep its payload one level down where a quick look misses it.
The admin area shows must-use plugins only on a separate Must-Use tab of the Plugins screen, and a must-use plugin can hide that tab entirely with a single filter. The same code can hook the user query so a rogue administrator does not appear on the Users screen, or remove itself from the plugin lists. None of that changes what is on disk.
Where to look
Work over SSH or a file manager with hidden files shown. Every check here only reads. WP-CLI commands that load WordPress also run every must-use plugin, so read the filesystem first.
Figure 1 · Reading the folder, not the screen
Two files the host can explain, one written last week by the site’s own user.
ls -la --time-style=+%F wp-content/mu-plugins
-rw-r--r-- 1 acct acct 1840 2024-03-11 host-cache-purge.php
-rw-r--r-- 1 acct acct 920 2024-03-11 host-staging.php
-rw-r--r-- 1 acct acct 4417 2026-10-12 wp-qa7k2.php
grep -cE 'base64_decode|eval|show_advanced_plugins' wp-content/mu-plugins/*.php
wp-content/mu-plugins/host-cache-purge.php:0
wp-content/mu-plugins/host-staging.php:0
wp-content/mu-plugins/wp-qa7k2.php:3
wp-config.php can point WordPress at a different must-use directory:
grep -nE 'WPMU_PLUGIN_DIR|WPMU_PLUGIN_URL|WP_CONTENT_DIR' wp-config.php
If either constant is set, the folder that loads is wherever it points — check that location as well as the default.
Every PHP file directly in the folder, with full timestamps:
find wp-content/mu-plugins -maxdepth 1 -type f -name '*.php' -exec ls -la --time-style=full-iso {} +
Each of these runs on every request. Note the owner, size and modification time of every one.
Files below the top level, which load only if a top-level file includes them:
find wp-content/mu-plugins -mindepth 2 -type f -ls
A subfolder that belongs to a host or agency tool usually has a top-level loader named after it. A subfolder no top-level file mentions, or hidden files with a leading dot, need explaining.
A legitimate must-use plugin names itself, and usually its author, in a Plugin Name comment:
head -n 20 wp-content/mu-plugins/*.php
A file with no header still loads; WordPress simply lists it under its filename. No header, a random name, or one long line of code is a reason to read the whole file.
Encoding, indirect execution, and the filters a loader uses to hide itself or an account:
grep -nE 'eval[[:space:]]*\(|base64_decode|gzinflate|str_rot13|assert[[:space:]]*\(|create_function|show_advanced_plugins|all_plugins|pre_user_query' wp-content/mu-plugins/*.php
show_advanced_plugins hides the Must-Use tab; all_plugins edits the plugin list; pre_user_query can drop accounts from the Users screen. Legitimate code uses these rarely, and never alongside encoded strings.
Host or attacker
Many managed WordPress hosts install their own must-use plugins for caching, staging, updates, or security, and agencies use them for site-wide settings. A must-use plugin is not suspicious in itself.
A host’s file usually carries the host’s name in its header or folder, and the host’s documentation or support can confirm it. Ask before removing anything you cannot place.
Legitimate must-use plugins are ordinary PHP with comments and functions. A file that is mostly an encoded string, or builds code from fragments before running it, has no innocent explanation.
A host’s file appears when the site was set up or migrated and changes when the host updates it. One written inside the incident window, by the site’s own user, is part of the incident.
A host has no reason to hide the Must-Use tab or an administrator account. Code that does either is concealing something.
Removing it
A loader in mu-plugins is often the piece that reinstalls everything else, so remove it before cleaning the layers it feeds — after preserving it.
Copy the whole must-use directory, with timestamps, outside the web root, and hash every file:
mkdir -p ~/incident
cp -pr wp-content/mu-plugins ~/incident/
find ~/incident/mu-plugins -type f -exec sha256sum {} +
The modification times bound the window everything else in the account gets checked against.
Delete the top-level loader and anything in a subfolder it included. Leave files your host or agency has confirmed; if you are unsure about one, move it outside the web root rather than deleting it.
With the loader gone, any administrator it was hiding becomes visible. Record and remove accounts nobody can vouch for, and check application passwords on the remaining administrators.
Something had write access to wp-content. Check the other layers below, then rotate the control panel, SFTP, database and WordPress credentials, and the keys and salts in wp-config.php.
List the folder again and compare it with the preserved copy. A file that reappears means another persistence layer is still writing it.
What else to check
A must-use loader is one of several ways to run code on every request without a plugin being switched on, and an infection that uses one usually uses more. In the ClickFix incident documented on this site, the chain lived in obfuscated PHP files, base64 content in the options table, the active theme’s functions.php, and a .user.ini startup directive at the same time.
advanced-cache.php, object-cache.php and db.php in wp-content load even earlier, under names that look like infrastructure.
An escaped @include at the top of index.php or wp-config.php runs a hidden payload before WordPress itself.
Encoded loader content stored in options survives every file replacement.
A plugin folder named like a real plugin, or a modified hello.php, is the activated counterpart of a must-use loader.
Common questions
Not necessarily. Many managed hosts, migration tools and agencies create it and install their own must-use plugins. Read each file’s header and ask your host about anything you cannot place. A file with no header, encoded content, or code that hides plugins or users is the warning sign — not the folder itself.
You cannot from the admin area; must-use plugins have no activation state. Removing or moving the file out of the folder is the only way to stop it loading. Preserve a copy first.
No. The tab appears only when must-use plugins exist, and a must-use plugin can hide it with a filter. List the folder from the filesystem to be sure.
Not by themselves. WordPress loads only the PHP files directly in the folder. A file in a subfolder runs when a top-level file includes it, which is normal for larger legitimate must-use plugins and a common way for a loader to keep its payload out of sight.
Yes, with --status=must-use. But it loads WordPress, which runs every must-use plugin first — including the one you are investigating, which is then in a position to interfere with what you see. Read the folder from the filesystem first and treat that as the record.
Another layer is still writing it: a drop-in, an include in a core file, an auto_prepend_file directive, a database loader, a scheduled task, or credentials the attacker still holds. The layers have to come out together.