If you landed here from a rejection email that says plugin_updater_detected, skip to the second section. You don’t need to gut your SDK and you don’t need to build a second plugin.
If you’re earlier than that — considering Freemius for a plugin you intend to list on WordPress.org — read the whole thing first. Most of the pain in this process comes from not understanding the build model until after you’ve already fought the scanner twice.
I went through the WordPress.org review process with a Freemius-licensed plugin and got rejected repeatedly. Some of those rejections were my own fault, some were the scanner doing what it’s designed to do, and a couple were failure modes that produce a build that looks completely fine and isn’t. This is all of it in one place.
The build model, which nobody explains up front
This is the thing to internalize before anything else, because almost every mistake downstream comes from missing it.
You maintain one codebase. Freemius produces two plugins from it.
You upload your full source — including all premium code — to Freemius. Freemius generates two zips:
- A premium build, containing everything. This is what paying customers download from Freemius.
- A free build, with all premium code removed, not disabled. This is what goes to WordPress.org.
The removal is driven by annotations you put in your source. If you get the annotations wrong, Freemius doesn’t warn you — it just generates a free build with your paid code still in it, and WordPress.org rejects you for shipping locked features.
Two consequences that took me too long to absorb:
Fixes belong in the source tree, not in the generated zip. I patched an outgoing build twice and watched the next deploy reintroduce the identical rejection. Freemius regenerates from source every time. If you fix the artifact, you’ve fixed nothing.
That includes readme.txt. Freemius regenerates your readme from source on every build. I kept stripping a section out of the outgoing zip and it kept coming back, three builds in a row, because I was fixing the wrong file.
The plugin_updater_detected rejection
Here’s what the automated scan sends you:
freemius/includes/class-freemius.php[ERROR: plugin_updater_detected]: Plugin Updater detected. These are not permitted in WordPress.org hosted plugins. Detected:site_transient_update_plugins
freemius/includes/class-fs-plugin-updater.php[ERROR: plugin_updater_detected]: Plugin Updater detected. Detected:class FS_Plugin_Updater
freemius/includes/class-fs-plugin-updater.php[WARNING: update_modification_detected]: Detected code which may be altering WordPress update routines. Detected:pre_set_site_transient_update_plugins
My first instinct was to abandon Freemius and maintain a separate, licensing-free version of the plugin for the directory. Don’t. That’s an enormous, permanent maintenance burden to solve a problem you don’t actually have.
My second instinct was to delete the offending SDK files. Also don’t — and they tell you so directly, in the same email:
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.
Why it fires
The underlying rule is real and reasonable: a plugin hosted on WordPress.org must receive its updates from WordPress.org, not from a third-party server. Self-updaters are prohibited.
The Freemius SDK contains a self-updater, because it needs one for the premium build. In the free build that updater doesn’t engage — WordPress.org serves updates for the free version, and Freemius’ updater only activates under a premium license. But the scanner is a static analysis pass. It sees FS_Plugin_Updater and pre_set_site_transient_update_plugins in the file tree and flags them on sight, with no ability to evaluate whether they’re reachable.
Hundreds of plugins in the directory bundle this SDK. This is a routine flag, not a verdict.
What actually clears it
Four things, in roughly descending order of how much work they did for me:
1. Move the SDK to vendor/freemius/. If the SDK sits at your plugin root, move it. vendor/ is Freemius’ own recommended location and it’s what a reviewer explicitly asked me for — it signals that the SDK is a third-party dependency rather than your code. In my case this single change is what got the submission through the gate.
Your require line changes accordingly:
require_once dirname( __FILE__ ) . '/vendor/freemius/start.php';
Then grep your tree for any other reference to the old path, because a missed one is a fatal error on activation.
2. Update the SDK. A current SDK is a substantially better argument than an old one — Freemius actively maintains compliance with directory policy, so “I’m on the latest version” means something. I submitted on 2.13.4.
3. Set the compliance flags in your fs_dynamic_init() config:
'is_org_compliant' => true,
'is_premium' => false,
These belong in the free build. If is_premium is true in what you submitted, you submitted the wrong zip.
4. Reply on the existing email thread. Not a fresh submission, not a new email. Keep it brief and factual — something to the effect of: the flagged code is part of the Freemius SDK, it isn’t active in this build, is_premium is false, and updates for this version are served by WordPress.org. Ask for manual review.
The automated scan is a gate, not the review. Real people look at Freemius plugins and pass them.
Stripping premium code correctly
This is where the genuinely dangerous mistakes live, because every failure mode here is silent. Your build succeeds. Your premium code is just still in it.
WordPress.org’s requirement is absolute: the free build cannot contain premium code at all. Not present-but-disabled. Not gated behind a license check. Not there.
Your own gating helper does not work
I had my Pro features behind a helper of my own:
if ( csmon_is_pro() ) {
require_once __DIR__ . '/includes/pro/class-csmon-digest.php';
}
That’s a runtime check. The file is still on disk, the code is still in the zip, and Freemius’ build processor has no idea any of it was supposed to be removed.
The same applies to can_use_premium_code(). It is a Freemius function, and I was told at one point it was a safe guard, and it is not. It’s a runtime check too. It doesn’t cause anything to be stripped.
Use the annotations Freemius’ processor recognizes
Whole files and folders: the @fs_premium_only annotation in the plugin header docblock lists paths to exclude from the free build.
Blocks of code: wrap in is__premium_only() — note the double underscore.
if ( csmon_fs()->is__premium_only() ) {
require_once __DIR__ . '/includes/pro/class-csmon-digest.php';
}
Methods: suffix the method name with __premium_only.
Done properly, includes/pro/ doesn’t exist in the free build. Not empty — absent.
The three silent failures
All three of these produce a clean build with premium code in it.
Compound conditions aren’t recognized. This:
if ( csmon_fs()->is__premium_only() && $something_else ) {
is not something the processor will strip. Keep the condition to the bare call and nest anything else inside.
Prose in the wrong place in the header docblock breaks annotation parsing. I wrote a helpful explanatory comment in my plugin header, positioned it badly, and silently disabled @fs_premium_only parsing for the entire file. The header is parsed structurally. Don’t put narrative in it.
Method suffix typos. __premium_only — two underscores at the front, and it goes at the end of the method name. A single underscore does nothing and throws no error.
Verify rather than trust. After every build, unzip the free version and look. grep for your Pro class names, your Pro directory, your license-gated function names. If any of it is in there, the annotations didn’t take.
Locked UI is also a rejection
Stripping the code is necessary but not sufficient. The free build also can’t ship the interface to features it can’t run:
- Disabled form inputs with a “Pro” tooltip
- Pro badges on settings
- Menu items that lead to an upgrade wall
- Upsell CTAs in place of functionality
WordPress.org calls this trialware and treats it as a guideline violation. I had all four and had to remove them.
There’s a related trap on the marketing side. Your readme cannot promise features the free build doesn’t have. I found — during review prep, thankfully — that my website advertised two modules as free-tier while my code had both in includes/pro/. That mismatch is a rejection if it reaches the readme, and it’s a bad customer experience regardless. Decide which is right, then make the code, the readme, and the site agree.
Note that Freemius’ own upgrade prompts are fine. It’s your hand-built locked UI that’s the problem.
Configuration alignment
Three places have to agree, and when they don’t, things break in confusing ways.
Slug. Your WordPress.org slug, the slug in fs_dynamic_init(), and the slug in the Freemius dashboard must all match. Change one, change all three. If code and dashboard disagree, license checks fail.
Text domain. WordPress.org requires the text domain to match the slug. If you rename the plugin, that’s every __() and esc_html_e() call in the codebase — in my case around 330 of them.
Product name. Your Freemius product title should match your plugin name. Mine didn’t for a while, and it meant a user could install “Canary Site Monitor,” click upgrade, and land on a checkout page for a differently-named product. Not a rejection, just a conversion killer.
While you’re in the dashboard, check your premium_suffix too — it appears in the admin and is easy to leave pointing at an old brand name after a rename.
Packaging
The generated free zip has a wrapper folder. Freemius wraps its output in an extra directory. WordPress.org requires your-plugin-slug/ to be the single top-level entry in the submitted zip. When it isn’t, the scanner can’t find your main plugin file, can’t read the header, and rejects you with an error saying your plugin has no name. Strip the wrapper before submitting.
Submit the free build, not the premium one. Obvious, and I still nearly didn’t. Check is_premium and check for your Pro directory before you upload.
Watch for secrets in the premium build. Freemius issues a gatekeeper key for WordPress.org compliance, and it lives in your premium source. It is a secret. If your repository is public, or if you’re sending builds around for review, know where it is and rotate it if it’s been exposed.
The pre-submission checklist
Run through this against the actual free zip, not against your source:
- [ ] SDK lives in
vendor/freemius/, and no stale path references remain - [ ] SDK is on a current version
- [ ]
is_premium => falseandis_org_compliant => true - [ ] Premium directory is absent, not empty — verify by unzipping
- [ ] No
is__premium_only()calls with compound conditions in source - [ ] Plugin header docblock is clean, with annotations intact
- [ ] No disabled inputs, Pro badges, locked menu items, or hand-built upsell CTAs
- [ ] Readme describes only what the free build actually does
- [ ] Slug matches across .org,
fs_dynamic_init(), and the Freemius dashboard - [ ] Text domain matches the slug
- [ ]
your-plugin-slug/is the single top-level entry in the zip - [ ] Readme fixes were made in source, not in the outgoing build
The short version
Freemius and WordPress.org are compatible, and thousands of plugins prove it. The friction is that the scanner can’t tell inactive code from active code, and Freemius’ stripping mechanism fails quietly when you get its syntax slightly wrong.
So: move the SDK to vendor/, keep it current, argue the false positive from a defensible configuration, and never trust that your premium code was removed — unzip the build and check.
Canary Site Monitor is free on WordPress.org. The full six-rejection saga and the Subversion publishing process are separate posts.