If you’ve been building WordPress sites for a while you probably have a folder of code snippets you copy into every new project. I know I do. Mine has been growing for years.
It’s always the same handful of fixes. Clean up the header output, switch off features the site is never going to use, tighten a security default or two, and get rid of the little admin annoyances clients always seem to notice. None of it takes more than a minute to write. The annoying part is writing it again on the next project.
Most tutorials tell you to drop this stuff in your theme’s functions.php file. That works fine until you switch themes, or hand the site over to a client who installs a different one, or update a child theme and lose the lot. A much better home for them is the mu-plugins folder.
Below are various handy must-use plugins I’ve put together over the years. Each one is a single file that does one specific thing. Copy the ones you want, ignore the rest, or grab the whole collection from GitHub at the end of the article.
In this article
What Are MU-Plugins?
MU stands for “must-use”. Any PHP file you drop into wp-content/mu-plugins/ gets loaded automatically on every request, before regular plugins, and there’s no activation step at all.
There are a few things worth knowing before you start dropping files in there:
- You can’t deactivate them from the admin. There’s no activate or deactivate link. To turn one off you delete or rename the file. That’s actually a nice feature when you’re handing a site to a client and you don’t want them switching off your security tweaks by accident.
- They load first. MU-plugins run before regular plugins, so they’re a good spot for anything that needs to define a constant or set up a filter early.
- Only files in the root of the folder get loaded. WordPress doesn’t scan subdirectories. Drop in
my-snippet/my-snippet.phpand nothing happens. The file has to sit directly inmu-plugins/. - Activation hooks don’t fire. Anything using
register_activation_hookto run setup code won’t work here. None of my snippets need it, but it’s worth knowing if you try moving a regular plugin into the folder. - They load alphabetically, so name your files sensibly if load order matters.
- You won’t get update notifications, because there’s no repository to check against. You maintain them yourself.
You can see everything that’s loaded under Plugins → Must-Use in the admin. That’s why each of my files still has a proper plugin header. Without one the file still runs, it just shows up as an unnamed entry in the list.
If you want the full picture, the Must Use Plugins page in the Advanced Administration Handbook is the official reference. It covers things like changing the directory with WPMU_PLUGIN_DIR, and explains why the name is a leftover from WordPress MU rather than an accurate description of what the folder does.
How to Use These Snippets
Create the wp-content/mu-plugins/ folder if it doesn’t exist yet, then drop in whichever files you want. That’s it. There’s nothing to activate.
If you’d rather not use mu-plugins, all of these work fine as regular plugins. You can also paste the code into a child theme’s functions.php file or a code snippets plugin, just leave off the plugin header.
Cleanup and Performance
These are the ones I install without really thinking about it. None of them change how the site works for visitors. They just stop WordPress doing work and storing data nobody asked for.
Clean Head
WordPress prints a bunch of markup into your that most sites never use. An RSD link for remote publishing clients nobody has run in a decade. A Windows Live Writer manifest. Shortlinks, oEmbed discovery links, and a generator tag that tells everyone exactly which version of WordPress you’re on.
None of it is huge on its own. But it’s bytes on every single page load, and that generator tag is handing automated scanners a free hint. This one strips the lot.
Heads up: Two of these are worth a second thought. Removing feed_links kills RSS autodiscovery, so drop those two lines if anyone actually subscribes to your feed. And removing rest_output_link_wp_head only removes the discovery link, it doesn’t disable the REST API. Some tools rely on that link to find your endpoints.
Disable WP Emoji Support
WordPress ships a JavaScript file that converts emoji into images for browsers that can’t render them. In 2026 basically every browser can. So what you’re left with is an extra script, an extra stylesheet, and a DNS prefetch to s.w.org on every page.
This one goes further than the emoji lines in Clean Head. It also strips the emoji handling out of the classic editor, feeds and outgoing emails, plus the resource hint.
$url ) {
if ( strpos( $url, $emoji_svg_url_bit ) !== false ) {
unset( $urls[$key] );
}
}
}
return $urls;
}, 10, 2 );
} );
add_filter( 'emoji_svg_url', '__return_false' );
Heads up: This overlaps with Clean Head. Pick one or the other for the emoji stuff, or just leave the duplicate remove_action calls in. They’re harmless.
Disable Attachment Pages
Every image you upload gets its own page at a URL like /my-post/img_4021/. There’s nothing on it but the image. Google calls that thin content, and on an image-heavy site you can end up with thousands of them.
This plugin 301 redirects any attachment page to its parent post. If there’s no parent it sends them to the homepage.
Heads up: Newer installs have a wp_attachment_pages_enabled option that’s already switched off, but WordPress leaves it alone on sites that were set up before that change. This covers you either way.
Disable Image Sizes
WordPress creates a copy of every image you upload at each registered size. Between core, your theme and your plugins, one upload can easily turn into a dozen files. On a big media library that’s real disk space and real backup time.
This stops the generation completely. It also disables the big image threshold, so your originals stay exactly as you uploaded them.
Heads up: This is a niche one, not a general speed win. With no intermediate sizes there’s no srcset, so mobile visitors download your full-resolution originals. That’s a lot worse than the disk space you just saved. It only really makes sense if something else handles resizing for you, like an image CDN or an offloading service. On a normal site you’re better off trimming the registered sizes you don’t use.
Limit Post Revisions
By default WordPress keeps every revision of every post forever. On a site with a few hundred long posts that have been edited over the years, revisions can easily take up more rows in wp_posts than your actual content does. And they get dragged along in every backup and every migration.
Five is plenty to recover from a mistake without letting the table get out of hand.
post_type to
* use different limits per post type.
*/
add_filter( 'wp_revisions_to_keep', function ( $num, $post ) {
return 5;
}, 10, 2 );
Heads up: This only applies to revisions created from now on. The existing ones stay put until you clean them out with WP-CLI or a maintenance plugin. You could also use the WP_POST_REVISIONS constant in wp-config.php, but I prefer the filter because it gets passed the post object. That means you can keep more revisions for posts than pages, or switch them off entirely for a custom post type.
Privacy and Third-Party Requests
Each of these stops your site sending something to someone else without anyone deciding it should. That’s partly a speed thing, since every external request is a dependency on another company staying online. It’s also a compliance thing. Data you never send is data you never have to document.
Disable AI
WordPress 7.0 added AI features to core along with a wp_supports_ai() function. Any plugin or theme that wants to use the core AI client is supposed to check it first. There’s a matching wp_supports_ai filter, so you can switch the whole thing off in one line.
Plenty of sites shouldn’t have AI on by default. Client contracts and NDAs often say you can’t send content to third-party providers without approval, and GDPR wants a documented lawful basis before personal data goes near a model. If it’s on by default, an editor can wire up a connector before anyone has looked at any of that.
This covers core plus the two third-party offenders I run into most.
Heads up: The core filter only covers code that actually calls wp_supports_ai(). Plugins that bundle their own AI integration ignore it completely and have to be switched off in their own settings, which is why Jetpack and Elementor get separate lines. If you want something stronger, define WP_AI_SUPPORT as false in wp-config.php instead. That runs earlier and another plugin can’t override it.
Disable Avatars
Every avatar on the page is an HTTP request to Gravatar. On a post with fifty comments that’s fifty requests to a domain you don’t control, which is both a speed cost and a rendering dependency on someone else’s uptime.
There’s a privacy side to it too. Requesting an avatar sends a hash of the commenter’s email address to Automattic, along with the visitor’s IP and the page they’re on. That’s the sort of thing you’d rather not have to write into a privacy policy for a feature nobody asked for.
Discussion.
*/
add_action( 'admin_init', function () {
global $wp_settings_fields;
unset( $wp_settings_fields['discussion']['avatars'] );
} );
Heads up: Filtering the option instead of just unticking the box means it stays off even if someone toggles the setting later. That’s why I hide the setting as well. If avatars still show up, check your theme. Anything calling get_avatar() with force_display set to true will still render one.
Disable Yoast Dashboard Widget
Yoast SEO adds a dashboard widget with an SEO overview at the top and a feed of posts from Yoast.com underneath. The feed is the problem. Rendering it means an uncached HTTP request out to Yoast every time the dashboard loads, and that request includes your WordPress and PHP versions as query parameters. So you’re reporting your server setup to a third party every time someone glances at the admin.
The scripts and styles load whether the widget renders or not, so just removing the meta box leaves them behind.
Heads up: This relies on Yoast internals that could get renamed in any release. If the widget comes back after an update, check the meta box ID and the three asset handles against the current version. And to be fair to Yoast, they’re not unusual here, just the most common example. Plenty of plugins phone home from the dashboard and the same approach works on any of them once you know the handles.
Security and Hardening
None of these replace a firewall, decent passwords or staying updated. They just switch off features a lot of sites never use. A feature nobody uses is a feature nobody is keeping an eye on.
Enable Automatic Core Updates
WordPress installs minor and security releases automatically out of the box, and that’s a good default. Those releases ship under a no-breaking-changes policy, so applying them silently is genuinely low risk. Major versions are left alone, because they can introduce changes your theme or plugins rely on.
But automatic updates can be switched off in wp-config.php, and on a site you didn’t set up yourself there’s no telling what’s in there. Drop in this mu-plugin and core updates will be enabled regardless.
Filters are the trick here. A site that sets WP_AUTO_UPDATE_CORE to false in wp-config.php has already defined the constant, so you can’t redefine it from an mu-plugin. But core only uses that constant to set a default value, then passes it through these filters before deciding anything. The filters run last, so they win.
Check Dashboard → Updates to confirm it worked.
You should see a line saying: “This site is automatically kept up to date with maintenance and security releases of WordPress only”.
Heads up: There are two things that will genuinely stop this working though, and no filter can fix either. If WordPress finds a .git, .svn or .hg directory at or above your site root, it disables auto-updates completely. So this does nothing on a site you deploy with Git. Same goes if WordPress can’t write to its own files without FTP credentials.
One last thing worth thinking about. If your host disables core updates because they patch on their own schedule, overriding them isn’t automatically an improvement. Check what they actually do before you switch this on.
Disable User Enumeration
WordPress gives away usernames in four different places, and most hardening advice only closes one of them:
- The XML sitemap includes an author provider.
- The REST API answers
/wp-json/wp/v2/usersfor anyone who asks. - Author archives put the username right in the URL, and
?author=1redirects to it so you can walk the IDs one by one. - The login form returns a different error for a bad username than for a bad password, so you can check whether an account exists.
A valid username is half of a login. Closing one of those four and calling it done is arguably worse than doing nothing, because it feels like you solved the problem. So this one closes all four.
[\d]+)']
);
return $endpoints;
} );
/**
* Return a 404 for author archives.
*
* Runs before redirect_canonical() so that ?author=1 requests 404 rather than
* being redirected to /author/username/, which would leak the name in the
* Location header.
*/
add_action( 'template_redirect', function () {
if ( ! is_author() ) {
return;
}
global $wp_query;
$wp_query->set_404();
status_header( 404 );
nocache_headers();
}, 0 );
/**
* Return a generic login error.
*
* Stops the login screen from confirming whether a username exists.
*/
add_filter( 'login_errors', function () {
return __( 'Login failed. Please check your credentials and try again.' );
} );
The author archive handler runs on template_redirect at priority 0, and that priority is doing real work. Core’s canonical redirect runs on the same hook at priority 10. Getting in first means ?author=1 returns a 404 instead of a 301 with the username sitting in the Location header.
Heads up: If you have real author archives you want indexed, delete that block and keep the rest. The generic login error is worth thinking about too if your site has a lot of non-technical users. It removes the difference between “you typed your password wrong” and “that account doesn’t exist”, which is the whole point, but it does make support requests vaguer.
Disable XML-RPC
XML-RPC is the remote publishing interface WordPress had long before the REST API existed. Almost nothing uses it now, but xmlrpc.php is still sitting there on every install accepting requests. It’s one of the most reliably brute-forced files on the internet, mostly because system.multicall lets someone try hundreds of logins in a single HTTP request.
The pingback method is the other half of it. It can be pointed at a third party and used to bounce traffic off your server, with your site as the unwitting participant.
Heads up: The xmlrpc_enabled filter on its own isn’t enough, and this is the bit most snippets get wrong. It only disables methods that need authentication. The pingback methods never needed a login in the first place, so they keep working. That’s why I unset them separately. If your host lets you block xmlrpc.php at the server level, do that too, since it stops the request before PHP even runs.
Disable Application Passwords
Application passwords let external tools authenticate against the REST API without using a real password. They’re a genuinely good feature if you’re using them. If you’re not, they’re an authentication method sitting open on every account on the site, and one your client has never heard of and is never going to audit.
On a site where nothing is making REST requests from outside, switching them off removes a whole category of credential you’d otherwise have to think about.
Heads up: Check before you use this one. Mobile apps, headless frontends, deployment scripts, uptime monitors and some backup and migration plugins all authenticate this way. If the site quietly depends on it, something will break and the error message won’t make it obvious why.
Disable File Editor
The built-in plugin and theme editors let anyone with the right capability edit live PHP on your production server from a browser. If an admin account ever gets compromised, that editor is the quickest route from stolen password to permanent backdoor.
The usual advice is to define DISALLOW_FILE_EDIT in wp-config.php, and that’s still the best option if you can get to that file. Plenty of managed hosts don’t give you access, so this does it from mu-plugins instead with a fallback for when the constant is already defined.
That else branch matters more than it looks. Some hosts define DISALLOW_FILE_EDIT as false in a mu-plugin of their own, and once a constant is defined you can’t redefine it. The file_mod_allowed filter gives you a second shot at it.
Disable User Registration
The Anyone can register setting under Settings → General is one checkbox. One misclick, or one plugin doing something unexpected, and your site is open to public registration. Bots find that checkbox remarkably fast.
Filtering the option means registration is off no matter what’s in the database, and it stays off even if someone ticks the box.
Heads up: This is for single-site installs. Multisite handles registration through its own network setting, so use the Network Settings screen there instead.
Comments are either the best part of a site or a permanent moderation tax. Which one you get mostly depends on how much of the machinery around them you leave switched on.
There are plenty of plugins that rip comments out of WordPress completely. This one is more specific, and it’s what I use on the Total theme demos. Existing comments stay visible so visitors can see how the theme styles them, but nobody can actually post anything.
It’s surprisingly handy outside of demos too. Archived blogs, docs sites and portfolios often want the old discussion to stay readable without leaving the door open to spam.
403,
'back_link' => true,
)
);
}
return $comment_data;
} );
/**
* Add honeypot field to catch automated submissions.
*/
function wpexdc_comment_honeypot() {
if ( ! is_user_logged_in() ) {
echo '';
}
}
add_action( 'comment_form_logged_in_after', 'wpexdc_comment_honeypot' );
add_action( 'comment_form_after_fields', 'wpexdc_comment_honeypot' );
Logged-in users can still comment, which keeps it usable for internal review. If you want to block everyone, just remove the is_user_logged_in() check.
Disable Trackbacks
Trackbacks and pingbacks were a good idea in 2005. These days they’re almost entirely a spam vector. Closing them site-wide gets rid of a whole category of moderation work.
Heads up: This closes pings on new and existing content, but it doesn’t touch the XML-RPC pingback endpoint, which is a separate way into the same feature. Pair it with the next plugin if you want the whole thing gone.
Obfuscate Email Shortcode
Putting an email address on a page as plain text is an open invitation to scrapers. The usual workarounds involve JavaScript, or writing it out as “hello [at] example [dot] com”, which is ugly and annoying for actual humans.
WordPress has a built-in function for this that hardly anyone uses. antispambot() encodes the address as HTML entities. Browsers decode them without any fuss, so visitors see and click a normal email address, while a scraper reading the raw HTML just gets a wall of entity codes. This wraps it in a shortcode.
'',
'text' => '',
'link' => 'false',
'class' => '',
),
$atts,
'obfuscate_email'
);
$email = sanitize_email( trim( $atts['email'] ) );
if ( ! is_email( $email ) ) {
return '';
}
// antispambot() encodes the address as HTML entities, which browsers decode
// in both the href and the link text, so its output is not escaped again.
if ( ! filter_var( $atts['link'], FILTER_VALIDATE_BOOLEAN ) ) {
return $atts['class']
? sprintf(
'%2$s',
esc_attr( $atts['class'] ),
antispambot( $email )
)
: antispambot( $email );
}
return sprintf(
'%3$s',
antispambot( $email ),
$atts['class'] ? ' class="' . esc_attr( $atts['class'] ) . '"' : '',
$atts['text'] ? esc_html( $atts['text'] ) : antispambot( $email )
);
} );
Here’s how you’d use it:
[obfuscate_email email="hello@example.com"]
[obfuscate_email email="hello@example.com" link="true" text="Email us"]
[obfuscate_email email="hello@example.com" link="true" class="contact-link"]
The link attribute defaults to false, so you get a plain obfuscated address unless you ask for a mailto: link. The text attribute sets the link label and gets ignored when link is off.
Heads up: This raises the bar, it doesn’t make you invulnerable. A scraper that renders the page or decodes entities will still find the address. But it works very well against the simple regex-based harvesters that make up most of the problem.
Admin Experience
These don’t touch the frontend at all. They’re about handing over a site that feels finished, and about not having to scroll past three upgrade prompts to get to your own content.
Disable Admin Bar
The admin toolbar is genuinely useful, right up until it starts interfering with your frontend. It pushes the html element down by 32 pixels, which breaks sticky headers, full-height hero sections and anything using 100vh. It’s also in the way when you’re taking screenshots or reviewing a design.
One filter turns it off for everyone on the frontend and leaves the admin alone.
If you’d rather keep it for admins and hide it from everyone else, swap __return_false for a closure that returns current_user_can( 'manage_options' ).
Disable WP Events and News Dashboard Widget
The Events and News widget pulls WordPress community events based on the visitor’s location, plus news from the official blog. It’s a nice idea, but it means an external HTTP request on the dashboard and clients tend to find it confusing at best.
The same remove_meta_box approach works on the other core dashboard widgets. Swap in dashboard_quick_press, dashboard_activity or dashboard_site_health with the right context argument.
Hide Admin Notices
Open the plugins screen on a site running twenty plugins and you’ll find upgrade prompts, review requests, discount banners and setup wizards stacked several deep before you get to anything useful. It’s probably the most common complaint I hear from clients, and it makes a site you built look unfinished through no fault of your own.
This hides all of it from anyone who can’t manage options. Admins still see the notices that matter, editors and authors get a clean screen.
Heads up: This is a blunt instrument. It removes every callback on those hooks, including the legitimate ones, so a plugin reporting a form error through admin_notices will fail silently for non-admins. Test it against whatever your editors actually use. If you want something more surgical, target specific plugin callbacks by name instead of calling remove_all_actions().
Media Library File Size
The Media Library list view tells you the date, the author and the post an image is attached to. It doesn’t tell you how big the file is. When you’re trying to work out why the uploads folder has ballooned, that’s exactly the column you want.
Since WordPress 6.0 the file size is already sitting in the attachment metadata, so showing it costs nothing. No extra queries, no extra data, just a column reading something that’s already there.
Heads up: You’ll see an em dash for uploads from before WordPress 6.0 and for some file types where core never recorded a size. I’ve deliberately left the column unsortable. The size lives inside a serialized array, which you can’t order on numerically, and making it sortable means writing the size out to its own meta key for every attachment on the site. That’s a lot of stored data for a column you’re mostly scanning rather than sorting.
Staging and Demo Sites
Read this bit twice before you install anything from it. Both of these are the right call in the environment they’re meant for and actively harmful on a live site.
Disable Emails
Nothing ruins a Monday morning like finding out your staging site has been sending real order confirmations to real customers for a week. This short-circuits wp_mail() so no email leaves the site at all.
It’s the first thing I install on any staging clone or local copy of a live site.
Heads up: Don’t put this on production. It blocks password resets, new user notifications, WooCommerce order emails, contact form submissions and everything else, silently. Only use it where you’re certain nothing needs to send mail. And if you’d rather see what would have been sent instead of just dropping it, a mail logging plugin is the better tool.
Disable Password Reset
Password reset emails are one of the more common ways a site gets probed. They also cause real problems on demo sites, where one visitor resetting the shared demo account locks everyone else out.
It’s also the right move on sites where authentication happens somewhere else, like an SSO or LDAP setup, where the built-in reset flow can leave people with a password that doesn’t do anything.
403,
)
);
}
} );
Heads up: This is a genuine lockout risk on a normal site. Only use it where you’ve got another way back in. Admins can still set passwords manually from the user profile screen.
Get Them All on GitHub
All of these live in a single repository so you can browse the source and pull in whatever you need without copying and pasting out of a blog post.
Download it as a ZIP and copy across the files you want. That’s deliberately the only instruction I’m giving. Several of these are meant for staging sites rather than production, so installing all of them at once isn’t something I’d recommend on a site that matters.
If you’d rather work from a clone, keep it somewhere outside your web root and copy files across from there. Cloning straight into wp-content/mu-plugins/ leaves a .git directory sitting in a publicly reachable folder, and it’ll fail outright on hosts that already keep their own files in there.
Wrapping Up
None of these snippets are complicated, and that’s sort of the point. They’re mostly a few lines each, solving problems you’ve probably solved before. The difference is where they live. Stick them in mu-plugins and a theme switch won’t wipe them out.
If you’re not sure where to start, go with the safe ones. Clean Head, Disable Admin Bar, Disable File Editor and Disable Trackbacks are fine on pretty much any site. Then add the more targeted ones as you need them, and keep Disable Emails on your staging environments where it belongs.
I don’t run comments here, so if you’ve got a question about any of these, spot a bug, or keep something in your own mu-plugins folder that isn’t in the collection, the issues page on GitHub is the place for it.