I built a plugin to solve a problem I kept hitting on client sites. It detects database errors, plugin mass-deactivations, 404 floods, and PHP errors, and it reports them in plain English instead of stack traces. I tested it, I wrote the readme, I packaged it up, and I submitted it to the WordPress.org plugin directory.
Then I got rejected. Six times.
Not one of those rejections was about whether the plugin worked. Every single one was about compliance — naming rules, code conventions, packaging, and the specific ways a freemium licensing SDK collides with directory policy. The review process tests your patience and your attention to detail. It does not test whether you wrote good software.
Here’s every rejection I got, in order, and what each one actually cost to fix.
Round 1: The name started with “wp”
The plugin was called WP Canary. The domain was getwpcanary.com. The logo said WP Canary. All of it was wrong.
WordPress.org auto-rejects any plugin name or slug that begins with “wp,” and bans the word “WordPress” anywhere in the name. There is no appeal unless you own the trademark. It’s applied by a script before a human ever sees your submission.
I want to be clear about something, because I assumed it too: this is not a two-line fix. Changing a plugin’s name changes its slug, and the slug is load-bearing. Here’s everything it touched:
Plugin Name:in the main plugin file- The
=== Title ===line inreadme.txt - The text domain — WordPress.org requires it to match the slug, which meant updating roughly 330
__()andesc_html_e()calls across the entire codebase - The Freemius slug, in both
fs_dynamic_init()and the Freemius dashboard, which have to agree or license checks break - The admin menu slug
And that last one surfaced a bug that had been sitting there quietly. My admin class had hardcoded screen IDs:
'toplevel_page_wp-canary'
'wp-canary_page_wpc-database'
WordPress derives those strings from the menu slug. Change the slug, and those constants point at screens that no longer exist — which means the admin CSS and JavaScript stop loading on every plugin page, silently, with no error. I rewrote them to build from a MENU_SLUG constant so a future rename can’t desync them again.
New name: Site Canary. Resubmitted.
Round 2: The zip had a wrapper folder
Rejected with an error about the plugin having no name.
This one was packaging, not code. Freemius generates your free build for you, and it wraps the output in an outer folder. WordPress.org expects your-plugin-slug/ to be the single top-level entry in the zip. When it isn’t, the scanner can’t find your main plugin file, can’t read the header, and reports that your plugin has no name.
Strip the wrapper. Resubmit.
Round 3: The scanner flagged the Freemius SDK
freemius/includes/class-fs-plugin-updater.php[ERROR: plugin_updater_detected]: Plugin Updater detected. These are not permitted in WordPress.org hosted plugins.
This is the one that made me consider abandoning Freemius entirely and maintaining a second, separate free version of the plugin with no licensing code in it at all.
Don’t do that. It’s an enormous amount of ongoing work to solve a problem you don’t have.
Read the last paragraph of the rejection notice, because they tell you the answer:
The above may contain false-positives. If you believe an error or warning is incorrect or a false-positive, please do not work around it. A reviewer will manually confirm this during the review process.
The rule behind the flag is real: a plugin hosted on WordPress.org must receive updates from WordPress.org, not from a third-party server. But Freemius’ free build is designed around that rule — the updater only engages for premium licenses, and .org handles updates for the free version. The scanner sees FS_Plugin_Updater and pre_set_site_transient_update_plugins and flags them on sight. Hundreds of directory plugins bundle this SDK.
What I did, and what eventually got it through:
- Moved the SDK out of the plugin root into
vendor/freemius/. This is Freemius’ own recommended location, and it’s what the reviewer specifically asked for. This did more work than anything else on the list. - Updated the SDK to 2.13.4. A current SDK is a much better argument than an old one.
- Set
is_org_compliant => trueandis_premium => falsein the init config. - Replied on the existing email thread — not a fresh one — stating that the flagged code is part of the SDK, is inactive in the .org build, and that updates for this version are served by WordPress.org.
What I did not do, and what you should not do: gut or delete the SDK to get past the scanner. They explicitly forbid working around the check, and a stripped SDK breaks your licensing completely.
(If you’re not using Freemius, you can skip this round entirely — but the general lesson holds. The automated scan is a gate, not the review. Arguing a false positive from a compliant configuration works. Hacking around the scanner does not.)
Round 4: Plugin URI and Author URI were identical
Your plugin headers in the main plugin file have the same value for both the plugin and author URI. A plugin URI is a webpage that provides details about this specific plugin. An author URI is a webpage that provides information about the author of the plugin. Those two must be different.
Fair, and easy. Both of mine pointed at the product domain. I changed the author fields to point at me instead:
* Plugin URI: https://canarysitemonitor.com
* Author: Jen Pardee
* Author URI: https://profiles.wordpress.org/snarkles683/
The trap here is a build-system trap, and it caught me: the fix has to go into the Freemius source tree, not just the outgoing zip. Freemius regenerates the plugin from source on every deploy. Patch only the artifact and the next build reintroduces the exact same rejection.
Round 5: The human review letter
This is where a real person read the plugin, and the feedback arrived as a batch: the name was still wrong, the function prefix was too short, the free build shipped locked features, and there were script enqueue and opt-in issues.
The name, again
Two objections. “Canary” was flagged as a possible trademark, and “Site” was a generic word leading the name.
I decided to fight one and fold on the other.
“Canary” I contested. It’s a common English noun, it’s the entire basis of the product metaphor — the canary in the coal mine, an early warning that something’s wrong — and there’s no WordPress project by that name. That’s a winnable argument.
“Site” leading the name I gave up immediately, because the reviewers enforce distinctive-term-first consistently and their own guidelines spell it out. I would have lost, and a lost round costs weeks.
So I reordered it. Canary Site Monitor. Distinctive term first, descriptor after. I kept the bird, the metaphor, and the brand; I just stopped putting the generic word in front.
Two things about the reply email that matter more than they should:
Keep it short. They ask for brevity and they respond badly to argumentative walls of text. Mine was three sentences and a request.
Say the slug out loud. Changing the slug in your code is not enough — you have to explicitly ask the review team to reserve the new one in your reply, or your code and their records disagree.
The prefix
wpc_ and WPC_ were too short. Three characters isn’t enough to guarantee uniqueness against every other plugin a user might have installed, and the guidelines require a distinctive prefix of four or more.
I renamed everything to csmon_ / CSMON_. Everything means: functions, classes, constants, option names, database table names, AJAX action names, CSS classes, and DOM IDs. Mechanical work, but it has to be exact, and missing one AJAX action name is a silent runtime failure rather than a fatal error.
Locked features in the free build
This one is a real product constraint, not a formality. The free build on WordPress.org cannot contain premium code at all. Not disabled form inputs, not Pro badges, not locked menu items, not upsell CTAs pointing at features the code can’t run.
I’d been gating Pro features behind my own csmon_is_pro() helper. That doesn’t work, because the code is still there — the build process has no idea it should be removed.
The fix was converting to Freemius’ own annotations, which their build processor recognizes and strips: is__premium_only() blocks and the __premium_only method suffix. Done properly, includes/pro/ doesn’t exist in the free build at all.
Three things I got wrong on the way there, all of which produce a build that looks fine and isn’t:
can_use_premium_code()alone is not a safe guard. It’s a runtime check. It doesn’t cause code to be stripped.- Compound
&&conditions inside anis__premium_only()block aren’t recognized by the Freemius processor. Keep the condition simple. - Explanatory prose in the wrong position in the plugin header docblock breaks
@fs_premium_onlyannotation parsing. I wrote a helpful comment in the header and quietly disabled the entire stripping mechanism.
Every one of those failure modes is silent. The build succeeds. The premium code is just still in it.
Round 6: Into the queue
The corrected build passed the scanner, and the submission was accepted for review with 49 plugins ahead of me in the queue.
Forty-nine is a strange thing to feel good about, but after five rejections it was the first number I’d seen that was countable and moving in the right direction.
It was approved as Canary Site Monitor, slug canary-site-monitor.
What I’d tell someone starting this
Read the naming rules before you buy the domain. My original brand name was disqualified by an automated script for a reason I could have looked up in five minutes. That cost me two rounds, a domain, and a full text-domain rename across 330 strings.
Fix things in source, not in the artifact. Twice I patched the outgoing zip and watched the next build reintroduce the identical rejection. If you have a build system, the build system is where the fix lives.
Pick which objections to fight. Reviewers are consistent and their guidelines are public. When you get two objections and one of them is clearly enforced policy, concede it and spend your credibility on the one that’s actually arguable. Every round you lose is weeks.
Don’t work around the scanner. They tell you not to, and they mean it. Fix the underlying configuration or make the false-positive argument from a defensible position.
Assume the rename is never two lines. Slugs propagate into text domains, menu slugs, screen IDs, license configuration, and any string you hard-coded because you didn’t expect the name to change. Build them from constants now, while it’s cheap.
None of this made the plugin better. It made the plugin compliant, which is a different thing, and getting the two confused is what made the process feel so much longer than it was.
Canary Site Monitor is free on WordPress.org. Getting it published was a separate ordeal involving Subversion, which I’ve written about separately.