If your plugin is going to the WordPress.org plugin directory, write the scoped tree to
vendor-prefixed/ rather than to the default deps/. It is one configuration line, and it is the
difference between a submission that clears the automated gate and one that is blocked by errors
raised against code you did not write.
- Why the location matters
- What Plugin Check skips
- The recommended layout
- Loading the autoloader
- What the skip does and does not cover
- Making sure the tree is actually shipped
- Checking it before you submit
- Checklist
Since 1 October 2024, every new plugin submitted to the WordPress.org directory is run through Plugin Check before a human ever sees it. The announcement is explicit about what happens next:
If the new plugin has an error level item in this category, the submission will be blocked from being submitted for review, until it is fixed.
— Plugin Check and 2FA Now Mandatory For New Plugin Submissions
Plugin Check is largely PHPCS with the WordPress rulesets behind it. Pointed at your own code that
is exactly what you want. Pointed at Guzzle, Monolog or the AWS SDK it is not: general-purpose PHP
libraries use file_get_contents(), curl_*, fopen(), error_log(), base64_decode() and
serialize() as a matter of course, none of which they can be talked out of, and none of which you
can fix — the code belongs to somebody else and any edit is undone by the next scoping run.
A scoped tree in deps/ gets read as your code. The same tree in vendor-prefixed/ — a directory
Plugin Check carries in its ignore list for exactly this purpose — is skipped entirely.
Plugin Check keeps a list of directory names it never scans. As of the current release it is:
$default_ignore_directories = array(
'.git',
'vendor',
'node_modules',
'vendor_prefixed',
'vendor-prefixed',
);— Plugin_Request_Utility::get_directories_to_ignore()
The same list is what the CLI documents:
Additional directories to exclude from checks. By default,
.git,vendor,vendor_prefixed,vendor-prefixedandnode_modulesdirectories are excluded.
Three things follow from that list, and they are the whole reason this page exists:
vendor-prefixedandvendor_prefixedare on it. They are on it precisely because they are the names the ecosystem settled on for scoped dependencies — Yoast SEO's among them. Plugin Check carries them so that prefixed vendor code does not produce false positives. That is this exact use case, named in the tool.vendoris on it too. Anything under it, at any depth, is out of scope for the automated run.depsis not on it. This plugin's default output folder is scanned like the rest of your plugin. That default predates the mandatory check and is kept for compatibility; for a plugin headed to WordPress.org it is the wrong choice.
The list is filterable (wp_plugin_check_ignore_directories) and the CLI takes
--exclude-directories, but neither helps you here. WordPress.org runs the check on its own
infrastructure when you submit, so the only lever you have is where the files sit.
{
"extra": {
"wpify-scoper": {
"prefix": "MyPlugin\\Deps",
"folder": "vendor-prefixed"
}
}
}my-plugin/
my-plugin.php
composer.json
composer-deps.json
composer-deps.lock
vendor/ <- Composer's own, for your unscoped code
autoload.php
composer/
vendor-prefixed/ <- the scoped tree
scoper-autoload.php
autoload.php
guzzlehttp/
A sibling of vendor/ rather than a directory inside it. That is the cleaner shape for three
reasons:
- The two trees stay separate. One is Composer's, one is the scoper's, and the scoper
replaces its own wholesale on every run. Keeping them apart means
there is no path Composer might one day install a package into, and no way to point
foldersomewhere that would take Composer's vendor directory with it. - The name says what it is. Anyone reading your plugin — including a WordPress.org reviewer —
knows what
vendor-prefixed/holds without being told. - It survives build scripts that strip
/vendor. Plenty of release tooling excludes the Composer vendor directory by reflex; a scoped tree nested inside it disappears with it.
vendor_prefixed with an underscore is equally valid — it is on the same ignore list, and it is
what Yoast uses. Pick one and stay with it; do not use both.
Do not point folder at vendor/ itself. The run would delete Composer's own vendor directory.
The location changes, the two-autoloader rule does not:
<?php
/**
* Plugin Name: My Plugin
*/
require_once __DIR__ . '/vendor-prefixed/scoper-autoload.php';
require_once __DIR__ . '/vendor/autoload.php';
add_action( 'init', function () {
$client = new \MyPlugin\Deps\GuzzleHttp\Client();
} );scoper-autoload.php pulls in its own autoload.php, so that is still one require_once for the
scoped tree — only the path in front of it moved. vendor/autoload.php is still needed for your own
unscoped code, and add_action() is still called unprefixed.
If you are moving an existing plugin, this line is the one thing that breaks silently. Grep for it:
grep -rn "deps/scoper-autoload.php" --include="*.php" .Be clear about what you are buying. Plugin Check skipping vendor-prefixed/ is a scanner
exemption, not a licence. The plugin guidelines
apply to everything in your zip, bundled libraries included, and a human reviewer reads what the
scanner passed over.
| Covered | The PHPCS-derived checks — escaping, sanitisation, alternative functions, development functions, direct database access, i18n — stop being raised against library code you cannot change. |
| Not covered | Licensing. Everything you ship must be GPL-compatible, third-party libraries included. |
| Not covered | Unmaintained or abandoned libraries. Those are refused on their own merits, wherever they sit. |
| Not covered | Guideline 4 — no obfuscated or unreadable code. Scoped output is ordinary readable PHP with the namespaces rewritten, so this is satisfied by default. Do not minify or strip the tree to save space. |
| Not covered | Guideline 13 — do not bundle libraries WordPress already ships (jQuery, SimplePie, PHPMailer, PHPass, …). Scoping one does not make it acceptable; use the copy WordPress provides. |
Keep composer.json and composer-deps.json in the published zip. The reviewer guidance asks for
manifests to be left in place so the dependency set can be read, and yours is the only record of
what the scoped tree was built from.
Strip the development-only directories the
common issues page names —
node_modules, build tooling, tests, demos. Your scoped tree is not one of those: it is runtime
code and it has to ship.
vendor-prefixed/ is a build artifact like any other, so it has to be built before the deploy and
must not be filtered out of it. A release missing it fatals on activation with a missing class.
Three places to check:
.gitignore — this one needs a new line. /vendor/ no longer covers the scoped tree now
that it is a sibling rather than a child:
/vendor/
/vendor-prefixed/
/tmp-*Unless you deploy by pushing a git checkout, in which case commit the tree and see Deployment.
.distignore — used by 10up's deploy
action and most WordPress.org release
scripts. Neither /vendor nor /vendor-prefixed belongs in it: the first holds Composer's
autoloader, the second holds your dependencies, and the plugin needs both at runtime.
/.git
/.github
/.wordpress-org
/node_modules
/tests
/tmp-*
.distignore
.gitignore
composer.lock
.gitattributes — the fallback when there is no .distignore. Same rule: no export-ignore on
either directory.
Then build before you deploy, so the directory exists at all:
composer install --no-dev --optimize-autoloaderThat one command installs your unscoped dependencies into vendor/ and, on the post-install-cmd
it triggers, writes the scoped tree into vendor-prefixed/. --no-dev propagates to the scoped
run, so the require-dev block of composer-deps.json is left out of the release.
Run the same tool WordPress.org will. Plugin Check is itself a WordPress plugin, so it installs into a site and runs through WP-CLI:
wp plugin install plugin-check --activate
wp plugin check my-pluginwp plugin check also takes a path or a zip, which is the form you want here — point it at the
built plugin, the directory as it will be published with vendor/ and vendor-prefixed/
present, rather than at a clean checkout. A checkout with neither passes for the wrong reason:
wp plugin check /path/to/build/my-pluginIn a pipeline, use the Plugin Check action after your build step.
What you are looking for is that nothing in the report has a path under vendor-prefixed/. If
something does, the scoped tree is not where you think it is: re-read the
wpify-scoper: prefix …, folder … line that composer install -v prints.
-
folderisvendor-prefixed, notdeps - the autoloader
require_oncepoints at the new path, and no staledeps/path is left in any PHP file -
prefixis a namespace nobody else would pick — your own vendor namespace with\Depson the end -
/vendor-prefixed/is in.gitignore(unless you deploy by git push) -
composer install --no-dev --optimize-autoloaderruns in the release build - neither
/vendornor/vendor-prefixedis in.distignore, and neither carriesexport-ignorein.gitattributes -
composer.jsonandcomposer-deps.jsonare in the published zip -
node_modules, tests and build tooling are not -
wp plugin checkon the built directory reports nothing undervendor-prefixed/ - every scoped library is currently maintained and GPL-compatible
- nothing scoped duplicates a library WordPress already ships
- Configuration →
folder— the setting itself - Deployment — CI recipes, what to commit, multi-plugin repositories
- Getting started — the full first-run walkthrough