Every WordPress site we build or take over ends up with one file in wp-content/mu-plugins/. It has the site’s name at the top, a version number, the date it last changed, and a list of the plugins it replaced. Usually it is well under a hundred lines. It opens external links in a new tab, turns off comments, sends subscribers somewhere sensible after they log in.
None of that needs a plugin. All of it needs a home.
In Fewer Plugins, More Custom Code I argued that the long tail of one-job plugins costs more than it looks: another author, another update cycle, another settings screen, another thing to check when checkout slows down. This article answers the question that leaves open. If the small behaviours come out of plugins, where do they go?
My answer is a single site-specific mu-plugin. Here is why that folder, and what it costs.
What a mu-Plugin Is
mu-plugins (must-use plugins) are a native WordPress feature and have been since version 2.8. Any PHP file placed directly in wp-content/mu-plugins/ is loaded on every request, before the normal plugins. There is no activate button and no deactivate button. They do not appear in the regular plugins list, only under a separate “Must-Use” tab, and WordPress never offers to update them.
Two practical details catch people out. WordPress only loads PHP files sitting directly in the folder, not files in subfolders, so anything larger needs a small loader file. And because there is no dashboard switch, removing one means changing the filesystem.
The File
This is the shape of it. The site name is made up; the pattern is not.
<?php
/**
* Plugin Name: example.co.nz — Site Behaviour
* Description: Small site-specific behaviours. Replaces: External Links,
* Disable Comments, Login Redirect.
* Author: Managed Hosting Partners
* Version: 1.4.0
* Updated: 2026-09-12
*
* Deployed from Git. Do not edit on the server.
*/
// Open external links in a new tab.
add_filter( 'the_content', function ( $content ) {
$p = new WP_HTML_Tag_Processor( $content );
$home = wp_parse_url( home_url(), PHP_URL_HOST );
while ( $p->next_tag( 'a' ) ) {
$host = wp_parse_url( (string) $p->get_attribute( 'href' ), PHP_URL_HOST );
if ( $host && $host !== $home ) {
$p->set_attribute( 'target', '_blank' );
$p->set_attribute( 'rel', 'noopener' );
}
}
return $p->get_updated_html();
} );
// No comments or pingbacks anywhere on the site.
add_filter( 'comments_open', '__return_false', 20, 2 );
add_filter( 'pings_open', '__return_false', 20, 2 );
// Subscribers go to the homepage after login, not the dashboard.
add_filter( 'login_redirect', function ( $url, $request, $user ) {
if ( isset( $user->roles ) && in_array( 'subscriber', $user->roles, true ) ) {
return home_url();
}
return $url;
}, 10, 3 );
Three plugins replaced by one screen of code. The header does most of the work: the next developer opens the folder, reads the first ten lines, and knows what the site does differently, who owns it, and which plugins must not come back.
Four Places Ten Lines of Code Can Live
When a small behaviour comes out of a plugin, it usually lands in one of four places. Each has a failure mode.
The theme’s functions.php. This works until the site is redesigned. Behaviour that has nothing to do with how the site looks (login redirects, comment rules, tracking) disappears with the old theme, and nobody notices until someone asks why subscribers are landing in the dashboard again.
A code snippets plugin. The code lives in the database instead of on disk. It is outside version control, it has to survive encoding and migration, and the next developer has to know to look inside a plugin’s settings to find business logic. I have written about this in more detail in Why Custom Plugins Beat Code Snippets Every Time. It also replaces one plugin dependency with another.
A regular custom plugin. This is a reasonable choice, and far better than the first two. Its weakness is the deactivate link. Anyone with admin access can switch it off while troubleshooting, and “I turned off a few plugins to see if it helped” is a common enough support sentence to plan for.
A site mu-plugin. On disk, in Git, loaded on every request, with no switch in the dashboard. The behaviour is either deployed or it is not.
The deciding property is that last one. Behaviour the site depends on should not have an off switch in the dashboard.
Deployed, Not Edited
The header line that matters most is the last one: deployed from Git, do not edit on the server.
Changes go through the repository, get a version bump, and are deployed. The file on the server is a copy, not the original. That gives you a history of every behaviour change on the site (what changed, when, and why) and a way back if a change was wrong.
It also keeps the file honest. A quick fix typed into the server at 9pm is the kind of change nobody remembers six months later, and the one the next developer overwrites without knowing it was there.
What It Costs to Own the Code
Owning the code has a real cost, and it should be stated plainly: nobody upstream will fix it for you.
When we trial a PHP upgrade on a site and a regular plugin breaks, the usual answer is to wait for the plugin author to release a fix. When a site’s own mu-plugin breaks, no release is coming. Someone has to open the file and look.
That is why the rule has a size limit. The behaviours that go in the file are the ones you can read, understand and fix in a few minutes: a filter here, a redirect there, a script tag in the head. Ten lines is roughly the limit of what you can afford to own for years without thinking about it.
What Does Not Belong in It
This is not an argument for putting everything in mu-plugins.
Page builders, forms, ecommerce, SEO, caching and backups are real products with real maintenance behind them, and they belong in proper plugins. That is the subject of Only 3–5 Plugins Per Vertical Actually Matter.
A whole application does not belong in the site’s one file either. If the custom code has its own database tables, admin screens and tests, it deserves its own plugin, its own repository and its own release process. The site mu-plugin is for the small decisions, not the large ones.
The Audit Question
The plugin review in Fewer Plugins, More Custom Code ends with a question for each active plugin: what decision is it making for the site, and is that decision still active?
For the small ones, there is now a second question: does this belong in the site’s one file? If the answer is yes, move it, list the plugin in the header, and remove the plugin.
After a few rounds, the plugins list gets shorter and the one file gets a longer header. That is the right direction.
We run this review as part of our free site review at managedhosting.partners. If you are not sure what your plugins are doing, I am happy to take a look.



