plugin validate checks a plugin folder or .zip, or a marketplace and the plugins in it, without uploading anything. plugin pack and plugin publish run the same checks first. Each problem has a code; OpenAI’s plugin portal uses the same codes for the rules it shares with Enconvo.
validate exits with status 1 when there is an error, so you can run it in CI.
Severity
The review rules OpenAI checks at final submission, such as the number of test cases, stay warnings even with
--openai, because the portal’s upload accepts a plugin without them. So does mcp_servers_multiple: the upload takes a plugin with several MCP servers. So do undeclared_mcp_manifest_ignored and undeclared_app_manifest_ignored: the upload leaves the file out, but Codex still loads it. The final submission’s listing rules are reported only with --openai, as warnings.
Text fields
Most text fields are checked the same way, and their codes start with the field’s stem, such asplugin_display_name:
Addresses and colors
Addresses have to behttps:// addresses without a user name or password. The stems are plugin_homepage, plugin_author_url, plugin_website_url, plugin_privacy_policy_url, plugin_terms_of_service_url and plugin_support_url.
The listing’s four addresses (
websiteURL, supportURL, privacyPolicyURL, termsOfServiceURL) take up to 2,048 characters in the upload, so a longer one is <stem>_too_long (portal). Final submission takes up to 1,024, so one between the two limits is a <stem>_too_long warning, only with --openai.
Manifests
Paths to skills, MCP servers and apps
The stems areplugin_skills (skills), plugin_mcp (mcpServers) and plugin_apps (apps).
Images and other files
These apply to the listing’s images (logo, composerIcon, logoDark, composerIconDark, screenshots), the skill icons in agents/openai.yaml (icon_small, icon_large) and an Enconvo plugin’s icon.
Skills
Claude Code agents and commands
A Claude Code plugin’sagents and commands fields are checked as Claude Code loads them. Either field replaces its default folder.
The frontmatter of each command, agent and skill (the
--- block at the top of the file) is read as Claude Code reads it. Claude Code parses it as YAML, and when that fails it tries once more after putting top-level values that hold : or a YAML symbol such as @, * or # in quotes. Frontmatter that still fails loads with no metadata: a command or skill loses its description and every other field, and an agent keeps only the name its file gives it. Quote a value that holds : or starts with a symbol, at any level, to be safe.
Every
.md file under commands/, agents/ and the paths the manifest lists is checked, including hidden folders. A skill whose frontmatter, name or description already has a skill_* finding isn’t checked again. Only a plugin with a .claude-plugin/plugin.json is read this way.
Claude Code’s directory listing fields are checked as warnings, because Claude Code ignores them when it loads the plugin. Enconvo and the plugin store leave out a bad one.
Claude Code settings and channels
Claude Code doesn’t load a plugin whoseuserConfig or channels breaks its rules, so these are errors, matching claude plugin validate. Enconvo turns the settings into the plugin’s preferences. A .codex-plugin manifest isn’t checked for them.
MCP servers and apps
These apply to every MCP file the plugin has, and to the servers a Claude Code plugin declares inline in.claude-plugin/plugin.json.
Hooks
These apply tohooks/hooks.json, the files the manifest’s hooks names, and hooks written in the manifest itself, checked as claude plugin validate checks them and as Codex reads them. Each message says what Claude Code and Codex do about the problem. Claude Code reads a Claude Code plugin’s hooks; Codex reads the hooks of the manifest it loads (.codex-plugin/plugin.json, a portable plugin’s extensions["com.openai"].hooks, or a Claude Code plugin’s when it has no other manifest).
An error here means the plugin’s own app loses hooks: Claude Code applies none of a file’s hooks when it can’t read one under
PreToolUse or PermissionRequest, since that hook may be what guards the plugin, and Codex skips a whole file it can’t parse. When only the other app is affected, such as Codex reading a Claude Code plugin, or Claude Code ignoring one entry under another event, it is a warning.
Claude Code components
These apply to the LSP servers, monitors, output styles, workflows, themes, syntax highlighting languages and TypeScript declarations (types) in a Claude Code plugin’s .claude-plugin/plugin.json, checked as claude plugin validate checks them. Enconvo doesn’t load any of them, so they matter only for the plugin in Claude Code, but an error here means Claude Code doesn’t load the plugin at all. Like claude plugin validate, validate doesn’t open .lsp.json or a monitors file the manifest names.
Archives
When you validate a.zip, these are errors. When you validate a folder, validate checks the archive plugin pack would make from it, and reports the file rules as portal rules; the plugin_root_* rules apply only to a .zip. A file or folder in it that you don’t have permission to read is an archive_member_unreadable error for a folder too, because pack and publish can’t zip it: run chmod u+r on it (u+rx for a folder), or move it out of the plugin.
Marketplaces
validate checks a folder with a Claude Code .claude-plugin/marketplace.json or a Codex .agents/plugins/marketplace.json, or the catalog file itself. A Claude Code catalog gets the rules claude plugin validate applies. Each plugin in the marketplace’s folders is then checked as a plugin, and its problems start with plugins[N] (name):, with file paths from the top of the marketplace. A plugin that is also its own marketplace is checked once.
Enconvo package.json
Review materials
These check thereview object (in package.json, or extensions["com.openai"].review in the plugin.json OpenAI’s portal imports; see where they go) and, in that plugin.json, extensions["com.openai"].publication. See Review materials. A field set to null counts as left out, as it does in OpenAI’s import, which keeps the value saved there.
OpenAI’s final submission
OpenAI’s portal takes the uploaded plugin without these, then asks for them when you submit it to the directory.validate --openai and pack --openai report them as warnings, and plain validate leaves them out. They check the listing in .codex-plugin/plugin.json, plugin.json or .claude-plugin/plugin.json.
The directory shows the developer name of your verified OpenAI identity, whatever
interface.developerName says. Screenshots are only for an MCP server with its own interface; see screenshot_configuration_excluded and, from plugin scan --openai, screenshots_not_allowed.
Updates
An update replaces the whole plugin, on OpenAI’s portal as in Enconvo and Codex. To check a new version as an update, givevalidate or pack the release before it, as a .zip or a folder: the zip plugin pack made, or the one OpenAI’s portal gives you with Download release ZIP in the published version’s … menu.
validate --json lists the skills and MCP servers it compares under components.
Codes only OpenAI’s portal reports
OpenAI’s portal has a few codes thatvalidate doesn’t report, because they repeat a rule validate already checks under another code, or because only OpenAI can check them.
MCP server scan
validate reads the plugin’s files; plugin scan starts its MCP servers. It connects to each server the plugin declares, lists the server’s tools and checks them, as OpenAI’s portal does when you connect the plugin’s server. Scanning runs the plugin’s code, so scan only plugins you trust.
--openai, annotations_required is an error. scan finds servers where Enconvo does, in the manifests’ mcpServers, mcp.json and .mcp.json, and starts them as Enconvo, Codex and Claude Code do: a command starts in no particular folder unless it sets cwd, and gets only basic variables from your shell (HOME, PATH, SHELL, USER and a few more) plus the ones it names in env_vars and its own env. A server at a URL is reached over Streamable HTTP, with the headers it declares and the token in bearer_token_env_var. For a tool that shows its own interface, scan reads its view with resources/read, as Enconvo does, for the domains the view declares; the listing shows each view that embeds pages. scan exits with status 1 when there is an error, and doesn’t take a .zip: unzip it and scan the folder.
OpenAI’s portal no longer asks you to justify each tool’s annotations; if its automated review flags one, you can appeal with an explanation. It does ask for a justification of each page your views embed (
frame_domain_explanation_required). A server you have to sign in to is scanned there with the reviewer account you give in the Review details form.
Domain verification
When you connect a plugin’s MCP server in OpenAI’s portal, after the upload, the portal verifies that you control the server’s domain before you can submit the plugin. It gives you a token, and you serve that token at/.well-known/openai-apps-challenge on the MCP server’s host or on a parent domain: for https://mcp.example.com/mcp, at https://mcp.example.com/.well-known/openai-apps-challenge or https://example.com/.well-known/openai-apps-challenge. The URL answers with the token alone, as plain text: not a web page, JSON or a list of tokens. The path of the server’s URL doesn’t matter. If another plugin’s token is already at that URL, use a parent domain or a hostname of your plugin’s own rather than replacing it.
For each server at a public https:// URL, scan --openai reads those URLs and lists what each answers. Once the portal gives you the token, check it before you select Verify Domain:
scan exits with status 1 when none does (domain_challenge_mismatch).