
Hooks, plugins, and the price of extensibility
WordPress has about sixty thousand plugins and that is the reason it has sixty percent of the web, not the editor and not the themes.
The mechanism is simple enough to describe in a paragraph. Core calls do_action at named points and apply_filters when it has a value that might be modified. Anyone can register a callback against a name with a priority. Actions run for effect, filters transform a value and pass it on. That is the whole system.
What makes it work is that the names are stable. A plugin written in 2012 against save_post still runs, because removing a hook would break things nobody can enumerate. The result is an ecosystem with genuine longevity and a core that can never clean anything up.
The costs are all downstream of the same property.
Everything shares one process. A plugin that is slow makes the site slow, a plugin that throws takes down the request, and a plugin that wants to can reach into anything. There is no boundary because a boundary would have broken compatibility.
Ordering is a negotiation. Priorities are integers and everybody picks ten, so the actual order is registration order, which is plugin load order, which is alphabetical by directory. This produces bug reports that are genuinely hard to believe.
And the surface is unbounded. A hook name is a string. Nothing declares what a plugin may do, so what it may do is everything.
Neovim is instructive because it made the opposite trade with a similar architecture. Plugins are Lua, they run in the same interpreter, they can reach anything, and there is no sandbox at all. The difference is that nobody pretends otherwise. You install a plugin the way you install a program: deliberately, from a source you chose, understanding it runs as you.
That honesty is worth something. A sandbox that stops accidents but not attackers should be described as stopping accidents. Half the security value of plugin isolation in practice is people believing it is stronger than it is.
The other thing Neovim has that CMSes mostly do not is reload without restart. Change a plugin, source it, keep working. In a web server this is harder than it sounds because there are usually several worker processes and each has its own copy of everything, so reloading in one leaves the others serving the old code and requests alternate between them unpredictably.
If I were designing this again I would keep the flat hook model, because arrays of callbacks sorted by priority are fast and comprehensible, and I would resist the temptation to implement it with metatables, which people reach for in Lua and which is slower and stranger than the loop it replaces.
The part I would change is honesty about the boundary. Say clearly whether plugins are trusted. If they are, give them the whole API and stop pretending. If they are not, isolate them properly and accept that they lose access to most of what makes them useful. The middle position, where there is a restriction that looks like security and is not, is the worst of the three.