WordPress keeps every role in one autoloaded option, with each role’s capabilities copied out in full, because core has no inheritance. On a membership or commerce site with forty roles that option is read, unserialized and held in memory by every PHP worker on every request — and those are exactly the sites whose traffic is mostly logged in, so page caching never gets a look in.
LeanRoles does two things about it.
It measures. The auditor is strictly read-only. It takes the size of your role option with LENGTH() in the database rather than re-serializing it in PHP, times a real unserialize() of it on your machine after a warm-up pass, measures the resident footprint with the result kept alive, and works out what that costs in concurrent workers. It identifies roles that grant no effective permission, roles nobody holds, roles with identical capability sets, and capabilities it cannot account for — while being explicit that “unrecognised” is not “orphaned”, because custom code checks capabilities no scanner can see.
It offers the missing primitive. There is no way in WordPress to say “this user is a wholesale customer” without granting them permissions. There are only roles. That is why membership plugins keep inventing them: they have no alternative. A LeanRoles tag is that alternative. It appears in $user->roles, answers current_user_can(), and can be filtered on in WP_User_Query — and it is never written to the autoloaded option. Third-party code cannot tell the difference.
wp leanroles audit --format=jsonlibraries/user-tags/, that any plugin can bundle so tags cost them nothing to adoptLeanRoles uses Freemius for licensing, payment and automatic updates on its paid plan. Freemius is a third-party service, and this is the only thing in the plugin that talks to anything outside your site.
Nothing is sent anywhere unless you say so. The plugin asks once, on activation, and skipping is a real option: skip it and the plugin works exactly as it does otherwise. Nothing about the auditor or user tags depends on it.
If you do opt in, what is shared is:
Two further items are optional and stay off unless you tick them: the list of other plugins and themes installed on the site, and the newsletter.
Activating a licence key sends the key and the site URL, so the licence can be checked and updates delivered to you.
Freemius’ terms and privacy policy cover what they do with it.
What the paid plans cost, and what they add
It does not convert roles into tags for you. What it gives you are three independent primitives — create a tag, assign it in bulk, delete a role with reassignment — which compose into a conversion done by hand, by someone who has read the audit and accepts the risk.
Before deleting a role it tells you how many capabilities that role grants and how many users hold it. It does not tell you which of those users will actually notice the difference: answering that means computing effective capabilities per user, before and after, and the plugin does not do it. Take a backup and check for yourself.
The distributed plugin has no dependencies and no build step: what is in src/ and assets/ is what runs. Composer is used only for the test suite, and .distignore keeps all of it out of the release.
composer install
./tests/bin/start-db.sh # throwaway MariaDB on 127.0.0.1:3307
./tests/bin/install.sh # WordPress + the test library
composer test # single site
composer test:multisite # network
composer test:matrix # WordPress 5.9, 6.5 and latest
composer lint # standards + PHP 7.4 compatibility
Every test runs against a real WordPress install and a real database, because the risky parts of this plugin are all seams it does not own — the short-circuit contract of the metadata filters, what WP_User::set_role() does on its way past, how WP_User_Query builds a role clause. See tests/README.md.