> ## Documentation Index
> Fetch the complete documentation index at: https://docs.enconvo.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Validation Codes

> What each problem `enconvo plugin validate`, `scan`, `pack` and `publish` report means, how severe it is, and how to fix it.

`plugin validate` checks a plugin folder or `.zip`, or a [marketplace](#marketplaces) 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.

```sh theme={null}
npx -p @enconvo/api enconvo plugin validate [path]            # the folder, a .zip or a marketplace
npx -p @enconvo/api enconvo plugin validate [path] --openai   # OpenAI's portal rules become errors
npx -p @enconvo/api enconvo plugin validate [path] --json     # the result as JSON
npx -p @enconvo/api enconvo plugin validate [path] --previous <release>   # check an update against the release before it
```

Each problem is one line: its severity, its code, the file it is in, and what is wrong.

```text theme={null}
warning[plugin_short_description_too_long] .codex-plugin/plugin.json: interface.shortDescription is 42 characters; the limit is 30. (OpenAI's plugin portal)
error[skill_description_missing] skills/notes/SKILL.md: skills/notes/SKILL.md description is required.
```

`validate` exits with status 1 when there is an error, so you can run it in CI.

## Severity

| Severity | Meaning |
| - | - |
| `error` | Enconvo, Codex or Claude Code can't use the plugin as it is. `pack` and `publish` stop. |
| `warning` | The plugin works, but something is ignored or may not work in another app. |
| `warning … (OpenAI's plugin portal)` | Only OpenAI's portal applies the rule. With `--openai`, and in `pack --openai`, it is an error. |

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](#openais-final-submission) 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 as `plugin_display_name`:

| Code | Severity | Meaning |
| - | - | - |
| `<stem>_missing` | error when the field is required, otherwise portal | The field isn't set. |
| `<stem>_wrong_type` | error | The value isn't text. In a portable or Claude Code `plugin.json`, a `null` `version`, `description`, `author.name`, `author.email`, `author.url` or `homepage` is this error too, because Codex and Claude Code refuse the file. Leave the field out instead. |
| `<stem>_empty` | error | The value is empty or only spaces. |
| `<stem>_character_unsupported` | portal | The value has control characters, or line breaks in a one-line field. |
| `<stem>_too_long` | portal | The value is longer than the portal's limit below. |

| Stem | Field | Limit |
| - | - | - |
| `plugin_name` | `name` (required) | 64 |
| `plugin_id` | `id` in `.codex-plugin/plugin.json`, or `extensions["com.openai"].id` | |
| `plugin_version` | `version` | 64 |
| `plugin_description` | `description` | 1,024 |
| `plugin_author_name`, `plugin_author_email` | `author.name`, `author.email` | 120, 320 |
| `plugin_display_name` | `interface.displayName` (missing: `plugin_display_name_empty`) | 30 |
| `plugin_short_description` | `interface.shortDescription` | 30 |
| `plugin_long_description` | `interface.longDescription` | 4,000 |
| `plugin_developer_name` | `interface.developerName` | 120 |
| `plugin_category` | `interface.category` | |
| `plugin_capability` | each of `interface.capabilities` | 120 |
| `plugin_default_prompt` | each of `interface.defaultPrompt` | 128 |
| `skill_name` | `name` in `SKILL.md` (required) | |
| `skill_description` | `description` in `SKILL.md` (required) | 1,024 |
| `skill_agent_display_name`, `skill_agent_short_description`, `skill_agent_default_prompt`, `skill_agent_brand_color`, `skill_agent_icon_small`, `skill_agent_icon_large` | `interface` fields in `agents/openai.yaml` | |

## Addresses and colors

Addresses have to be `https://` 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`.

| Code | Severity | Meaning |
| - | - | - |
| `<stem>_format` (`plugin_author_url_not_https`) | portal | Not an `https://` address. |
| `<stem>_has_credentials` | portal | The address holds a user name or password. |
| `plugin_brand_color_format`, `plugin_brand_color_dark_format`, `skill_agent_brand_color_format` | portal | Not a six-digit hex color such as `#1A73E8`. |
| `plugin_brand_color_contrast`, `plugin_brand_color_dark_contrast` | portal | Less than 2:1 contrast against the light (`#FFFFFF`) or dark (`#212121`) background. |

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

| Code | Severity | Meaning |
| - | - | - |
| `plugin_manifest_missing` | error | The folder has no `package.json`, `plugin.json`, `.codex-plugin/plugin.json`, `.claude-plugin/plugin.json`, `manifest.json` or `SKILL.md`. |
| `plugin_manifest_not_file`, `plugin_manifest_unreadable`, `plugin_manifest_json_malformed`, `plugin_manifest_root_not_object` | error | The manifest isn't a readable UTF-8 file holding a JSON object. |
| `codex_manifest_parent_not_directory`, `codex_manifest_path_not_file` | error | `.codex-plugin` isn't a folder, or `.codex-plugin/plugin.json` in it isn't a file. |
| `plugin_name_format` | error | `name` has to start with a letter or digit and use only letters, digits, `-` and `_`, with single dots between them, as Codex and Claude Code take it, so `acme.tools` works and `acme.` or `acme..tools` doesn't. A portable `plugin.json`'s `name` follows Codex's rule instead: up to 64 lowercase letters, digits and single dots or hyphens, starting and ending with a letter or digit, so `acme.tools` works and `Acme_Tools` or `acme--tools` doesn't. OpenAI's portal takes no dots, so a dotted name in the manifest it reads is also an OpenAI portal finding (a warning, an error with `--openai`) that names the hyphenated name to submit, such as `acme-tools`. |
| `plugin_schema_missing` | error, or warning | The root `plugin.json` doesn't give `"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json"`, so Codex and OpenAI's portal don't read it as a portable plugin. A warning when `.codex-plugin/plugin.json` or `.claude-plugin/plugin.json` is there for them to read instead, an error otherwise. |
| `plugin_schema_unsupported` | error | `$schema` names another version of the Agent Plugins schema. Codex supports only 1.0.0 and refuses the plugin. |
| `plugin_version_not_semver` | error | `version` isn't a version number like `1.2.0`. |
| `plugin_author_wrong_type` | error | `author` in `plugin.json` has to be an object like `{"name": "…"}`. |
| `plugin_author_field_unsupported` | error | A portable `plugin.json`'s `author` has a field other than `name`, `email` and `url`, and Codex refuses it. |
| `plugin_repository_wrong_type`, `plugin_license_wrong_type` | error | `repository` or `license` in a portable or Claude Code `plugin.json` isn't text, such as `"https://github.com/acme/tools"` or `"MIT"`. Codex and Claude Code refuse an object or a `null`. |
| `plugin_keywords_wrong_type` | error | `keywords` in a `plugin.json` isn't a list of text. Codex and Claude Code refuse the file. |
| `plugin_schema_wrong_type`, `plugin_display_name_wrong_type`, `plugin_default_enabled_wrong_type`, `plugin_settings_wrong_type`, `plugin_dependencies_wrong_type` | error | In a Claude Code `plugin.json`, `$schema` or `displayName` isn't text, `defaultEnabled` isn't `true` or `false`, `settings` isn't an object, or `dependencies` isn't a list, `null` included. Claude Code doesn't load the plugin. |
| `plugin_dependency_invalid` | error | A Claude Code dependency isn't a plugin name (`"formatter"`), a name and marketplace (`"vault@acme"`), either followed by a version range (`"vault@acme@^1.2"`), or `{"name": "vault", "marketplace": "acme"}`. Names and marketplaces start with a letter or digit and use letters, digits, `.`, `_` and `-`. |
| `plugin_homepage_invalid` | error | A Claude Code `plugin.json`'s `homepage` isn't a web address, so Claude Code doesn't load the plugin. An `http://` address loads, with the `plugin_homepage_format` warning the listing gives it. |
| `plugin_metadata_wrong_type`, `plugin_binaries_wrong_type` | warning | `metadata` or `binaries` in a Claude Code `plugin.json` isn't an object, so Claude Code ignores it. |
| `plugin_binary_invalid` | warning | A binary's name isn't a lowercase file name like `rg`, or its `sha256` isn't the file's SHA-256 as 64 lowercase hex digits, so Claude Code doesn't fetch it. |
| `plugin_binaries_too_many` | warning | `binaries` lists more than 16 files. Claude Code fetches the first 16 (of the first 64) and ignores the rest. |
| `plugin_binaries_not_fetched` | warning | The plugin lists `binaries`. Claude Code fetches them into `bin/` when it installs the plugin; Enconvo doesn't, so hooks and servers that run them work only in Claude Code. |
| `plugin_extensions_wrong_type` | warning | `extensions` or `extensions["com.openai"]` isn't an object, so Codex and Enconvo ignore it and take the listing from `.codex-plugin/plugin.json`, if there is one. |
| `plugin_manifest_field_unsupported` | warning | A portable `plugin.json` has a field the Agent Plugins schema doesn't define. Put app data under `extensions`. In a Claude Code `plugin.json`, a field Claude Code doesn't read, as `claude plugin validate` reports it: the message names the field you likely meant (`descripton` → `description`), the marketplace entry it belongs in (`category`, `tags`, `source`, `strict`), `experimental` for `evals` and `syntaxHighlighting`, `userConfig` for `user_config`, or the manifest it comes from, such as npm's `type` or VS Code's `engines`. Listing fields such as `icon`, `screenshots` and `privacyPolicyUrl`, `extensions`, and `interface` and `apps` when Codex reads this file aren't reported. |
| `component_declaration_ignored` | warning | A portable plugin declares `skills` or `mcpServers` in `plugin.json`, in `extensions["com.openai"]`, or in `.codex-plugin/plugin.json` (a `skills` other than `./skills/`, or servers written out there). Codex, OpenAI's portal and Enconvo take its skills from `skills/` and its MCP servers from `mcp.json`; servers in `.codex-plugin/plugin.json` only pass their `env_vars` to the `mcp.json` servers of the same name. |
| `codex_overlay_ignored` | warning | `plugin.json` has `extensions["com.openai"]`, so Codex ignores `.codex-plugin/plugin.json`. Keep the listing in one of them. |
| `plugin_manifest_name_mismatch`, `plugin_manifest_version_mismatch` | warning (error in `publish`) | The manifests for different apps give different names or versions. |
| `plugin_interface_missing` | portal | There is no listing (`interface`). |
| `plugin_developer_missing` | portal | Neither `author.name` nor `interface.developerName` says who made the plugin. |
| `plugin_interface_wrong_type` | error | `interface` isn't an object. |
| `plugin_capabilities_wrong_type`, `plugin_capabilities_too_many` | error, portal | `interface.capabilities` isn't a list of text, or has more than 20 entries. |
| `plugin_default_prompt_wrong_type`, `plugin_default_prompt_entry_wrong_type` | error | `interface.defaultPrompt` isn't text or a list of text. |
| `plugin_default_prompt_too_many`, `plugin_default_prompt_duplicate` | portal | More than 3 example prompts, or the same prompt twice, ignoring letter case, spacing and Unicode form. |
| `plugin_default_prompt_mention` | portal | An example prompt @mentions an MCP server, which OpenAI's starter prompts can't. |
| `plugin_category_unknown` | portal | `interface.category` isn't one of OpenAI's categories: Productivity, Creativity, Developer Tools, Business & Operations, Data & Analytics, Communication, Education & Research, Security, Finance, Healthcare, Travel, Entertainment or Other. Without a category, OpenAI lists the plugin under Other. |
| `plugin_onboarding_skill_wrong_type` | error | `extensions["com.openai"].onboardingSkill` isn't a path. |
| `plugin_onboarding_skill_missing`, `plugin_onboarding_skill_not_skill_file`, `plugin_onboarding_skill_not_included` | portal | `onboardingSkill` has to point at the `SKILL.md` of one of the plugin's skills, like `./skills/get-started/SKILL.md`. Enconvo also takes the skill's folder. |
| `plugin_logo_path_missing`, `plugin_composer_icon_path_missing` | portal | `interface.logo` or `interface.composerIcon` isn't set. |
| `plugin_runtime_surface_missing` | error | The plugin has nothing to run: no skill, MCP server or app. |
| `dxt_version_missing` | warning | A desktop extension's `manifest_version` isn't set. |
| `dxt_server_missing` | error | A desktop extension's `server` doesn't say how to start its MCP server. |

### Paths to skills, MCP servers and apps

The stems are `plugin_skills` (`skills`), `plugin_mcp` (`mcpServers`) and `plugin_apps` (`apps`).

| Code | Severity | Meaning |
| - | - | - |
| `plugin_skills_directory_missing`, `plugin_mcp_file_missing`, `plugin_apps_file_missing` | error | The path points at nothing. |
| `<stem>_path_wrong_type`, `<stem>_path_empty` | error | The value isn't a path, or is empty. |
| `<stem>_path_not_directory`, `<stem>_path_not_file` | error | The path points at a file where a folder belongs, or the other way round. |
| `<stem>_path_unsupported` | portal | Codex takes only `./skills/`, `./.mcp.json` and `./.app.json`. |
| `undeclared_mcp_manifest_ignored` | warning | A Codex plugin has `.mcp.json` but `mcpServers` doesn't point at it. Codex and Enconvo load it anyway, but OpenAI's portal leaves it out of the upload. |
| `undeclared_app_manifest_ignored` | warning | A Codex plugin has `.app.json` but `apps` doesn't point at it. Codex loads it anyway, but OpenAI's portal leaves it out. Point at it if the plugin uses the app, or remove the file. |

## 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`.

| Code | Severity | Meaning |
| - | - | - |
| `declared_asset_path_wrong_type`, `declared_asset_path_empty`, `declared_asset_path_has_outer_whitespace`, `declared_asset_path_has_control_character` | error | The path isn't usable text. |
| `declared_asset_path_unsafe`, `declared_asset_path_outside_package` | error | The path is absolute or leaves the plugin's folder. |
| `declared_asset_file_missing`, `declared_asset_not_regular_file` | error (warning for an Enconvo icon) | The file doesn't exist or isn't a regular file. |
| `branding_asset_path_missing_root_prefix` | portal | The path has to start with `./`. |
| `image_file_format_unsupported`, `image_file_too_large` | portal (error for an Enconvo icon) | Not a PNG, JPEG, WebP or SVG (an Enconvo icon may also be a GIF), or larger than 5 MB. |
| `image_file_unreadable` | portal | The image can't be read. |
| `svg_xml_malformed` | portal | The SVG isn't UTF-8 text or well-formed XML: every tag has to close, inside one root element. |
| `svg_root_element_invalid` | portal | The file isn't an SVG image. |
| `svg_dimensions_missing`, `svg_dimensions_not_numeric`, `svg_dimensions_not_positive` | portal | The SVG needs a `viewBox` or a `width` and `height`. |
| `svg_dimensions_not_square`, `svg_dimensions_too_small` | portal | The SVG has to be square and at least 48 pixels. |
| `raster_image_decode_failed`, `raster_image_extension_content_mismatch` | portal | The image can't be read, or its contents don't match its extension. |
| `raster_image_not_square`, `raster_image_dimensions_too_small`, `raster_image_dimensions_too_large` | portal | The image has to be square, from 48 to 4,096 pixels. |

## Skills

| Code | Severity | Meaning |
| - | - | - |
| `skill_manifest_not_regular_file`, `skill_manifest_unreadable`, `skill_manifest_invalid_utf8` | error | `SKILL.md` isn't a readable UTF-8 text file. |
| `skills_path_unreadable` | error | You don't have permission to open the skills folder or a skill's folder (Enconvo's code). |
| `skill_frontmatter_missing`, `skill_frontmatter_unclosed`, `skill_frontmatter_yaml_malformed`, `skill_frontmatter_wrong_type` | error | `SKILL.md` has to start with front matter: a `---` line, `name` and `description` as YAML, and another `---` line. |
| `skill_identity_duplicate` | error | Two skills have the same `name`. |
| `skill_identity_too_long` | portal | `<plugin>:<skill>` is longer than 64 characters. |
| `skill_body_empty` | portal | `SKILL.md` has no instructions after its front matter. |
| `skill_directory_hidden` | portal | A skill's folder name starts with a dot. |
| `skill_manifest_nested`, `skill_manifest_missing` | portal | Each skill has to be a folder right inside `skills/`, with a `SKILL.md`. |
| `skill_file_ignored`, `skill_symlink_ignored` | warning | A file outside a skill folder, or a symbolic link, is left out. |
| `skill_metadata_ignored` | warning | `SKILL.md` sets its listing in `metadata`; put it in `agents/openai.yaml`. |
| `skill_agent_not_regular_file`, `skill_agent_unreadable`, `skill_agent_invalid_utf8`, `skill_agent_yaml_malformed`, `skill_agent_top_level_wrong_type` | portal | `agents/openai.yaml` isn't a readable UTF-8 YAML object. |
| `skill_agent_interface_missing`, `skill_agent_interface_wrong_type` | portal | `agents/openai.yaml` needs an `interface` section. |
| `skill_agent_policy_wrong_type`, `skill_agent_allow_implicit_invocation_wrong_type` | portal | `policy` isn't an object, or `allow_implicit_invocation` isn't `true` or `false`. |
| `skill_agent_dependencies_wrong_type`, `skill_agent_dependency_unsupported` | portal | `dependencies` takes only `tools`. |

## Claude Code agents and commands

A Claude Code plugin's `agents` and `commands` fields are checked as Claude Code loads them. Either field replaces its default folder.

| Code | Severity | Meaning |
| - | - | - |
| `plugin_agents_path_wrong_type`, `plugin_commands_path_wrong_type` | error | The field isn't a path or a list of paths (for `commands`, or a map of command names). |
| `plugin_agents_path_missing`, `plugin_commands_path_missing` | error | A listed path doesn't exist. |
| `plugin_agents_path_not_file` | error | `agents` lists a folder. Claude Code takes agent `.md` files only, so list each one. |
| `plugin_agents_file_not_markdown`, `plugin_commands_file_not_markdown` | error | A listed file, or a command's `source`, isn't a `.md` file. |
| `plugin_command_entry_wrong_type` | error | An entry in the `commands` map isn't an object. |
| `plugin_command_entry_source_or_content` | error | A `commands` map entry needs exactly one of `source` and `content`. |
| `plugin_command_entry_content_empty` | error | A command's `content` is empty. |
| `default_agents_folder_ignored`, `default_commands_folder_ignored` | warning | The plugin has an `agents/` or `commands/` folder, but the field doesn't list it, so nothing in it is installed. |

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.

| Code | Severity | Meaning |
| - | - | - |
| `plugin_component_frontmatter_invalid` | error | Claude Code can't parse the frontmatter. The message gives the line and what to change, such as a quote that isn't closed, a nested value with `: ` in it, or a line indented more than the keys around it. |
| `plugin_component_frontmatter_wrong_type` | error | The frontmatter is a list or a single value instead of `key: value` lines. |
| `plugin_component_frontmatter_missing` | warning | A command or agent has no frontmatter, so Claude Code shows it without a description. |
| `plugin_component_description_missing` | warning | A command or agent has no `description`, which tells people and Claude when to use it. A skill gets `skill_description_missing`. |
| `plugin_component_field_wrong_type` | error | `description` or `name` isn't text, `allowed-tools` isn't text or a list of tool names, or `shell` isn't text. YAML reads `name: 5` as a number and `name: true` as a boolean, so put such values in quotes. |
| `plugin_component_shell_invalid` | error | A command's or skill's `shell` isn't `bash` or `powershell`. |
| `plugin_component_metadata_ignored` | warning | A command's or skill's `metadata` isn't `key: value` pairs, so Claude Code drops it. |
| `plugin_component_unreadable` | error | A command or agent file can't be read. |
| `plugin_root_context_ignored` | warning | The plugin has a `CLAUDE.md` or `CLAUDE.local.md` at its root. Neither Claude Code nor Enconvo loads it as context; put that context in a skill (`skills/<name>/SKILL.md`). |

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.

| Code | Severity | Meaning |
| - | - | - |
| `plugin_icon_wrong_type` | warning | `icon` isn't a path. |
| `plugin_icon_path_invalid` | warning | `icon` doesn't start with `./`, leaves the plugin with `..`, or links to a file outside it. |
| `plugin_icon_file_missing` | warning | `icon` isn't a file in the plugin. |
| `plugin_icon_format_unsupported` | warning | `icon` isn't a `.png`, `.jpg`, `.jpeg`, `.webp`, `.svg` or `.gif` image. |
| `plugin_listing_url_invalid` | warning | `documentationUrl`, `supportUrl`, `privacyPolicyUrl` or `termsOfServiceUrl` isn't an `https://` address, or holds a user name or password. |

## Claude Code settings and channels

Claude Code doesn't load a plugin whose `userConfig` 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.

| Code | Severity | Meaning |
| - | - | - |
| `user_config_wrong_type` | error | `userConfig` (at the top level or in a channel) isn't an object of settings by key. |
| `user_config_key_invalid` | error | A setting key isn't letters, digits and `_`, or starts with a digit. |
| `user_config_setting_wrong_type` | error | A setting isn't an object. |
| `user_config_field_unsupported` | error | A setting has a field other than `type`, `title`, `description`, `required`, `default`, `options`, `multiple`, `sensitive`, `min` and `max`. |
| `user_config_type_invalid` | error | `type` isn't `string`, `number`, `boolean`, `directory` or `file`. |
| `user_config_field_wrong_type` | error | `title` or `description` is missing or isn't text, `required`, `multiple` or `sensitive` isn't `true` or `false`, `min` or `max` isn't a number, or `default` isn't text, a number, `true` or `false`, or a list of text. |
| `user_config_options_invalid` | error | `options` is on a setting that isn't a plain `string` one (or is `multiple` or `sensitive`), or isn't a non-empty list of text up to 64 characters that differs in more than letter case, with no control, invisible or non-plain space character and no space at either end. Its `default` has to be one of them; without a `default`, set `"required": true`. |
| `channels_wrong_type` | error | `channels` isn't a list. |
| `channel_wrong_type` | error | A channel isn't an object. |
| `channel_field_unsupported` | error | A channel has a field other than `server`, `displayName` and `userConfig`. |
| `channel_server_missing` | error | A channel doesn't name its MCP server in `server`. |
| `channel_display_name_wrong_type` | error | A channel's `displayName` isn't text. |
| `user_config_reference_undeclared` | error | An MCP server's `command`, `args`, `env`, `url` or `headers` names `${user_config.KEY}` for a key that isn't a top-level setting or a setting of that server's channel. Claude Code drops the server or passes the text through, and Enconvo fills in an empty value. |
| `user_config_reference_in_headers_helper` | error | An MCP server's `headersHelper` names `${user_config.KEY}`. Claude Code never fills settings into this shell command, so read the value in the helper script. |

## 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`.

| Code | Severity | Meaning |
| - | - | - |
| `mcp_manifest_unreadable`, `mcp_manifest_json_malformed`, `mcp_manifest_wrong_type`, `plugin_mcp_path_not_file` | error | The MCP file isn't a readable UTF-8 JSON object. |
| `mcp_servers_wrong_type` | error | The server list isn't an object keyed by server name. |
| `mcp_server_name_empty`, `mcp_server_wrong_type` | error | A server has an empty name, or isn't an object. |
| `mcp_server_transport_missing` | error | A server needs a `command` to run or a `url` to connect to. |
| `mcp_servers_missing` | warning | The servers aren't under `mcpServers`. Servers listed at the top work in Enconvo, Codex and Claude Code, but the Agent Plugins schema puts them under `mcpServers`. Servers under `mcp_servers` work only in Enconvo. |
| `mcp_server_type_missing` | warning | A server in a portable `mcp.json` needs `"type"`: `stdio`, `streamable-http` or `sse`. |
| `mcp_server_type_unsupported` | warning | A server's `type` (or Enconvo's older `transport`) is one Enconvo doesn't connect to, such as `ws`, so it skips the server. Enconvo runs `stdio`, `http` (or `streamable-http`) and `sse` servers. |
| `mcp_server_url_type_missing` | error | A server Claude Code reads has a `url` but no `type`, so Claude Code takes it for a local server without a `command` and drops it. Add `"type": "http"` (or `"sse"`). |
| `mcp_server_type_unknown` | error | A server Claude Code reads has a `type` it doesn't know, so it drops the server. Use `stdio` (or no type) for a command, and `http`, `sse` or `ws` for a url. |
| `mcp_server_type_reserved` | error | A server Claude Code reads uses a type only Claude Code itself registers (`sdk`, `sse-ide`, `ws-ide`) or only claude.ai connectors use (`claudeai-proxy`), so it drops a plugin's. |
| `mcp_server_field_wrong_type` | error | A field of a server Claude Code reads has the wrong shape, as `claude plugin validate` reports it: an empty or missing `command`, `args` that aren't a list of text, `env` or `headers` values that aren't text, a `url` that isn't text, a malformed `oauth`, `tools` or `toolPermissions`, a `timeout` that isn't a positive whole number, or an `alwaysLoad` that isn't true or false. Claude Code drops the whole server. Fields it doesn't know pass. |
| `mcp_server_cwd_outside` | warning | A portable server's `cwd` has to start with `./`, `${PLUGIN_ROOT}` or `${PLUGIN_DATA}`. |
| `mcp_server_url_invalid` | error | A remote server's `url` isn't an absolute address such as `https://host/path`, and holds no `${VAR}`, so it never connects. An empty `url` isn't checked. |
| `mcp_server_url_insecure` | warning | A remote server's `url` uses `http://` or `ws://` to a host other than `localhost`, a `.localhost` name, `127.0.0.1` or `[::1]`, so its requests, headers and any credentials travel in cleartext. Use `https://` or `wss://`. A url whose host is a variable isn't checked. |
| `mcp_server_header_credential` | warning | A value in a remote server's `headers` (or Codex `http_headers`) looks like a real credential: a known token shape such as `sk-…`, `ghp_…`, `AKIA…` or a JWT, or a long, varied value under an authorization, API key, token, secret, password or credential header. Everyone who installs the plugin can read it, so reference a sensitive setting (`${user_config.KEY}`) or an environment variable (`${VAR}`) instead. Values with a variable or placeholder text such as `your-api-key` pass. |
| `mcp_server_oauth_resource_ignored` | warning | A remote server's `oauth_resource` isn't on the server's own origin, or has a fragment, so Enconvo signs in without it. |
| `mcp_server_auth_metadata_insecure` | warning | A remote server's `oauth.authServerMetadataUrl` doesn't use `https://`. Claude Code refuses it; Enconvo reads `http://` only from `localhost`, for testing. |
| `mcp_server_approval_mode_invalid` | error | A server's `default_tools_approval_mode` or a tool's `approval_mode` isn't `auto`, `prompt`, `writes` or `approve`. Codex won't load the file. |
| `mcp_server_tools_wrong_type` | error | A server's `tools` isn't an object of tool settings by tool name, or a tool's settings aren't an object. Codex can't load a server whose `tools` is a list, the form Claude Code takes, and Claude Code drops an `http` or `sse` server whose `tools` is Codex's object (`mcp_server_field_wrong_type`), so give each its own MCP file or remove `tools`. |
| `mcp_server_approval_approve_ignored` | warning | A server or tool asks for `approve`. Codex runs those tools without asking, but Enconvo asks as it would for any tool; only the person turns questions off, with the Agent's Permissions chip. |
| `mcp_servers_multiple` | warning | The plugin has more than one MCP server. OpenAI's portal connects only one of them per plugin to scan and review it. A server declared in both `mcp.json` and `.mcp.json` under the same name counts once. |
| `app_manifest_unreadable`, `app_manifest_json_malformed`, `app_manifest_wrong_type`, `plugin_apps_path_not_file` | error | `.app.json` isn't a readable UTF-8 JSON object. |
| `app_entries_missing`, `app_entries_wrong_type`, `app_entry_wrong_type` | error | `.app.json` needs an `apps` object of app entries. |
| `app_id_missing`, `app_id_wrong_type` | error | An app entry needs an `id`. |
| `app_id_format` | portal | An app's `id` has to be the ID OpenAI gave it, starting with `asdk_app_`, `connector_` or `templated_apps_`. |
| `app_entry_required_wrong_type`, `app_entry_optional_wrong_type` | portal | An app's `required` or `optional` has to be `true` or `false`. An app is needed unless it says `"required": false` or `"optional": true`. |
| `duplicate_app_reference` | warning | Two app entries have the same `id`. Codex and OpenAI's portal keep only the first. |
| `app_configuration_excluded` | portal | OpenAI's portal can't take a plugin with app references (`apps` or `.app.json`) yet. Put the MCP server's address in `.mcp.json` and finish the app's setup in OpenAI's dashboard. Enconvo, Codex and Claude Code still install the plugin. |
| `hooks_configuration_excluded` | portal | OpenAI's portal can't take a plugin with lifecycle hooks (the manifest's `hooks`, `hooks/hooks.json` or `hooks.json`) yet. Leave them out of the package you upload there; Enconvo's store takes them. |
| `screenshot_configuration_excluded` | portal | `interface.screenshots` is for a plugin with an MCP server and its own interface; OpenAI's portal refuses it in a plugin that has only skills. |

## Hooks

These apply to `hooks/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).

| Code | Severity | Meaning |
| - | - | - |
| `hooks_unreadable`, `hooks_json_malformed` | error | A hooks file isn't readable UTF-8 JSON. Claude Code doesn't load the plugin at all, and Codex skips the file. |
| `hooks_wrong_type` | error | A hooks file, or its `hooks`, isn't an object. Claude Code and Codex skip every hook in it. |
| `hooks_events_outside` | error or warning | Events such as `PreToolUse` are at the top of the file, beside `hooks`, instead of inside it: `{"hooks": {"PreToolUse": […]}}`. Codex skips the file, and so does Claude Code when one of them is `PreToolUse` or `PermissionRequest`, or the file has no `hooks`; it ignores other stray events. |
| `hooks_events_missing` | error | A hooks file has no `hooks` object of events, so Claude Code applies none of it. |
| `hooks_field_unsupported` | error or warning | A hooks file has `surface`, which Claude Code no longer takes, or another field than `description` and `hooks`, which Codex refuses: it skips every hook in the file. |
| `hooks_field_wrong_type` | error | A hooks file's `description` isn't text, or its `modules` isn't a list of one hooks module. |
| `hook_event_unknown` | warning | Claude Code or Codex doesn't send the event, so it ignores those hooks. Codex sends 12 events: 11 of Claude Code's, such as `PreToolUse`, `PostToolUse`, `UserPromptSubmit`, `SessionStart` and `Stop`, and its own `Interrupt`. |
| `hook_matchers_wrong_type`, `hook_matcher_invalid`, `hook_entry_invalid` | error or warning | An event isn't a list of matchers, a matcher isn't `{"matcher": "…", "hooks": […]}` with text in `matcher`, or a hook isn't an object with a `type`. |
| `hook_type_unknown` | error or warning | A hook's `type` isn't one Claude Code (`command`, `prompt`, `agent`, `http`, `mcp_tool`) or Codex (`command`, `prompt`, `agent`, `mcp_tool`) knows. Codex skips the whole file over a type it doesn't know, such as `http`. |
| `hook_field_wrong_type` | error or warning | A hook misses a field its type needs (`command`, `prompt`, `url`, or `server` and `tool`) or has one of the wrong kind, such as a `timeout` that isn't a number above 0 (Codex needs a whole number), a `url` that isn't absolute, or `headers` values that aren't text. |
| `hook_type_unsupported` | warning | A `prompt` or `agent` hook in a Codex plugin: Codex reads it but doesn't run it yet. |
| `hook_args_ignored` | warning | A `command` hook Codex reads has `args`. Codex has none, so it runs the `command` alone: write the whole command line there, with each path in double quotes. |
| `hook_command_placeholder_unquoted` | warning | A shell command uses `${CLAUDE_PLUGIN_ROOT}`, `${CLAUDE_PLUGIN_DATA}` or `${CLAUDE_PROJECT_DIR}` outside quotes, so it breaks when the path has a space. Write `"${CLAUDE_PLUGIN_ROOT}/scripts/run.sh"`. |
| `plugin_hooks_wrong_type` | error or warning | The manifest's `hooks` isn't a path to a hooks file, the hooks themselves, or a list of them, or the list mixes paths and hooks, which Codex ignores. |
| `plugin_hooks_path_invalid`, `plugin_hooks_path_missing`, `plugin_hooks_path_not_file` | error | A path in `hooks` doesn't start with `./`, points at nothing, or points at a folder. |
| `hooks_inline_format_conflict` | warning | Hooks written in a manifest have the other app's shape: Claude Code's `hooks` holds the events themselves, Codex's a whole hooks file, `{"hooks": {…}}`, and each ignores the other's. Put them in `hooks/hooks.json`, which both read. |
| `hooks_file_ignored` | warning | `hooks/hooks.json` isn't loaded by Codex because its manifest names other hooks (Claude Code always adds the file), or the plugin has a `hooks.json` at its root, which neither app loads unless the manifest names it. Name the file in `hooks`, or move it to `hooks/hooks.json`. |

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.

| Code | Severity | Meaning |
| - | - | - |
| `plugin_component_not_loaded` | warning | The plugin has LSP servers, monitors, output styles, workflows, themes or syntax highlighting languages, which only Claude Code runs. Enconvo installs the rest of the plugin. |
| `plugin_component_path_invalid` | error | A path in `lspServers`, `outputStyles`, `workflows`, `types`, `experimental.themes`, `experimental.monitors` or `experimental.outputStyles` doesn't start with `./`, or, for LSP servers and monitors, doesn't end in `.json`, or, for `types`, in `.d.ts`. |
| `plugin_component_path_missing` | error | A path points at nothing, which Claude Code reports as a load failure. A path with `..` or `\` is `declared_asset_path_unsafe`. |
| `plugin_component_wrong_type` | error | A component isn't a path, inline LSP servers or monitors, or a list of them, `types` isn't a path, or `experimental.evals` or `experimental.outputStyles` isn't a path or a list of paths. |
| `plugin_component_not_experimental` | warning | `themes` or `monitors` is at the top of the manifest. Claude Code still loads it there for now, but asks for `experimental.themes` and `experimental.monitors`. |
| `plugin_experimental_wrong_type`, `plugin_experimental_field_unsupported` | warning | `experimental` isn't an object, or holds a key other than `themes`, `monitors`, `outputStyles`, `evals` and `syntaxHighlighting`; the message names the key you likely meant. Claude Code ignores it. |
| `plugin_experimental_field_not_loaded` | warning | `experimental.outputStyles` is set. Claude Code accepts it, but loads output styles only from `outputStyles` and `output-styles/`. |
| `plugin_syntax_highlighting_invalid` | error | `experimental.syntaxHighlighting` isn't `{"hljsLanguages": [{"id": "hcl"}]}` with up to 16 highlight.js languages and no other keys. Each language takes an `id` of up to 64 lowercase letters, digits, `_` and `-`, starting with a letter, an optional `remote` of up to 256 characters (`npm:<package>[@<version>]` or `github:<owner>/<repo>@<ref>#<path>.js`), and an optional `integrity` (a `sha256-`, `sha384-` or `sha512-` hash in base64). |
| `lsp_server_wrong_type`, `lsp_server_field_unsupported` | error | An inline LSP server isn't an object, or has a key Claude Code doesn't take. It takes `command`, `extensionToLanguage`, `args`, `transport`, `env`, `initializationOptions`, `settings`, `workspaceFolder`, `startupTimeout`, `shutdownTimeout`, `requestTimeout`, `restartOnCrash`, `maxRestarts` and `diagnostics`. |
| `lsp_server_command_missing`, `lsp_server_command_has_spaces` | error | An LSP server has no `command`, or its `command` has a space and isn't an absolute path. Put arguments in `args`: `{"command": "gopls", "args": ["serve"]}`. |
| `lsp_server_extensions_invalid` | error | `extensionToLanguage` is missing or empty, has a key without a leading `.`, or maps one to something other than a language name: `{".go": "go"}`. |
| `lsp_server_field_wrong_type` | error | `args` isn't a list of text, `env` an object of text, `transport` `stdio` or `socket`, a timeout a whole number above 0, `maxRestarts` a whole number of 0 or more, or `restartOnCrash` or `diagnostics` true or false. |
| `monitor_wrong_type`, `monitor_field_unsupported`, `monitor_field_missing` | error | A monitor isn't an object, has a key other than `name`, `command`, `description` and `when`, or lacks `name`, `command` or `description` as text. |
| `monitor_when_invalid` | error | `when` isn't `always` or `on-skill-invoke:` followed by a skill name. |
| `monitor_name_duplicate` | error | Two monitors share a `name`. |
| `user_config_reference_in_monitor_command` | error | A monitor's `command` names `${user_config.*}`. `claude plugin validate` passes it, but Claude Code refuses to start the monitor, because the command runs through a shell. Have the script read the value itself. |
| `default_output_styles_folder_ignored`, `default_workflows_folder_ignored`, `default_themes_folder_ignored`, `default_monitors_file_ignored` | warning | The manifest names output styles, workflows, themes or monitors, so Claude Code ignores the default `output-styles/`, `workflows/`, `themes/` or `monitors/monitors.json` unless the manifest lists it too. LSP servers in the manifest add to `.lsp.json` instead. |

## 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.

| Code | Meaning |
| - | - |
| `archive_format_not_zip`, `archive_empty`, `archive_member_unreadable` | The file isn't a readable, non-empty zip. |
| `archive_too_large`, `archive_uncompressed_too_large`, `archive_too_many_entries` | Over 100 MB, over 512 MB unpacked, or over 5,000 entries. |
| `archive_member_too_large` | One file is over 100 MB. |
| `archive_member_type_unsupported` | A link or special file; an archive holds only files and folders. |
| `archive_member_path_empty`, `archive_member_path_has_outer_whitespace`, `archive_member_path_has_backslash`, `archive_member_path_absolute`, `archive_member_path_has_empty_segment`, `archive_member_path_has_parent_segment` | A file's path is empty, absolute, uses `\` or `..`, or has stray spaces or empty parts. |
| `archive_member_path_too_deep` | A path is more than 20 folders deep. |
| `archive_member_path_duplicate`, `archive_member_path_normalization_collision`, `archive_member_path_type_conflict` | Two paths are the same, differ only in letter case or Unicode form, or one is both a file and a folder. |
| `plugin_root_missing`, `plugin_root_ambiguous`, `plugin_root_has_siblings` | The archive has to hold one plugin, at its top or in one folder there, with nothing next to it. |
| `archive_metadata_ignored` (warning) | macOS Finder data (`__MACOSX`, `.DS_Store`) is left out. Make the archive with `plugin pack`. |
| `archive_member_secret` (warning) | A file looks like a private key or a login: an SSH key (`id_rsa`, `id_ed25519`), a `.p12` or `.pfx` file, a `.pem` or `.key` file with a private key in it, or a `.netrc`, `.npmrc` or `.pypirc` with a password or token. Everyone who installs the plugin gets a copy, so move it out of the plugin folder and have the plugin ask for it in a setting or an environment variable. `pack` and `publish` always leave out `.env` files. |

## 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.

| Code | Meaning |
| - | - |
| `marketplace_json_invalid` | The catalog isn't a JSON object. |
| `marketplace_name_missing`, `marketplace_name_invalid` | The marketplace needs a name of letters, digits, `.`, `_` and `-`, starting with a letter or digit, with no spaces, `..` or non-ASCII letters. Codex takes no dots in a marketplace name, so a dotted name is an error in a Codex catalog, and a warning in a Claude Code catalog when the folder has no `.agents/plugins/marketplace.json`, because Codex then reads the Claude Code one. Use `acme-tools` rather than `acme.tools`. |
| `marketplace_name_reserved` | Claude Code keeps names such as `inline`, `npm` and `github`, and names that start with `claudeai-` (error). The official Anthropic names are a warning, because Claude Code takes them only from Anthropic's repositories. `org`, `org-provisioned` and `unknown` are a warning too: Claude Code takes them, but Claude Desktop's managed marketplace sync rejects the whole marketplace. |
| `marketplace_name_impersonates` | A Claude Code marketplace named like one of Anthropic's own: `anthropic` or `claude` followed by `marketplace`, `plugins`, `official` or `directory` (`claude-plugins-hub`), or `official` next to either (`acme-claude-official`), also when the words are split with separators (`cl-aude-marketplaces`). Claude Code refuses to add the marketplace, so lead with your own name, such as `acme-claude-plugins`. Codex takes the name, and Enconvo still installs the marketplace. |
| `marketplace_name_too_long` (warning) | The name is longer than 128 characters. Claude Code takes it, but Claude Desktop's managed marketplace sync rejects the marketplace. |
| `marketplace_owner_missing` | A Claude Code catalog needs `owner.name`. |
| `marketplace_description_missing`, `marketplace_display_name_missing` (warning) | Claude Code shows the description; Codex shows `interface.displayName`. |
| `marketplace_field_unknown` (warning) | Claude Code ignores this top-level or `metadata` field. The message names the field you likely meant, such as `plugins` for `plugin`. |
| `marketplace_field_wrong_type` | A top-level field Claude Code can't read, so it can't add the marketplace: a `$schema`, `version` or `description` that isn't text, an `owner.email` or `owner.url` that isn't text, a `forceRemoveDeletedPlugins` that isn't `true` or `false`, an `allowCrossMarketplaceDependenciesOn` that isn't a list of marketplace names, or a `metadata` that isn't an object or whose `version` or `description` isn't text. `renames` that isn't an object of old plugin names to a new name or `null` is this error too; Claude Code then ignores the renames. |
| `marketplace_rename_unresolved` | An old name in `renames` doesn't lead to a plugin in the catalog or to `null`: the chain loops, ends at a name that isn't in `plugins`, or takes more than 16 steps. |
| `marketplace_plugins_missing`, `marketplace_plugins_empty` | `plugins` isn't a list, or it is empty (a warning). |
| `marketplace_plugin_root_invalid` | `metadata.pluginRoot` has to be a relative path inside the marketplace. |
| `marketplace_plugin_invalid`, `marketplace_plugin_name_missing`, `marketplace_plugin_name_invalid`, `marketplace_plugin_duplicate` | An entry isn't an object, has no usable name, or repeats another's. An entry's name follows the plugin `name` rule: letters, digits, `-` and `_`, with single dots between them. |
| `marketplace_plugin_name_too_long` (warning) | The entry's name is longer than 128 characters. Claude Code takes it, but Claude Desktop's managed marketplace sync drops the entry. |
| `marketplace_plugin_field_wrong_type` | Claude Code lists the plugin but can't install it, because an entry field has the wrong type: a text field such as `version`, `description`, `license`, `repository` or `category` that isn't text, a `homepage` that isn't a URL, `keywords` or `tags` that aren't lists of text, a `strict` or `defaultEnabled` that isn't `true` or `false`, an `author` without a `name`, a `dependencies` entry Claude Code can't read, `headers` values that aren't text, or a component field of the wrong shape, such as a `skills` path without `./`, an `agents` file that isn't `.md`, or an `mcpServers` file that isn't `.json` or `.mcpb`. The same checks apply inside `experimental`. An entry's `userConfig` and `channels` get the `user_config_*` and `channel*` checks a `plugin.json` gets. |
| `marketplace_plugin_field_unknown` (warning) | Claude Code ignores this field in the entry, its `experimental`, its `relevance` or `relevance.signals`. The message names the field you likely meant, or the manifest the field comes from. |
| `marketplace_plugin_field_ignored` (warning) | The entry's `experimental`, `metadata` or `relevance` isn't an object, so Claude Code ignores it. |
| `marketplace_plugin_relevance_invalid` | `relevance` needs `signals` naming at least one of `cli` (up to 10 command names), `hosts` (up to 20 lowercase host names, like `registry.terraform.io`), `filesRead` and `cwd` (up to 10 patterns with forward slashes), or `manifestDeps` (1 to 10 `{"file", "pattern"}` regular expressions), and an optional `topic` of 1 to 64 characters. The message says whether Claude Code can't install the plugin or only `claude plugin validate` refuses it. |
| `marketplace_plugin_source_missing`, `marketplace_plugin_source_invalid` | The source is missing or can't be fetched: a path with `..` or outside the marketplace, a bare name without `metadata.pluginRoot`, a `sha` that isn't a full commit, an `archive` that isn't a public https zip or has a `sha256` that isn't 64 hex digits, or a git address Enconvo can't clone. |
| `marketplace_plugin_source_unsupported` (warning) | Enconvo can't install a `command` source. |
| `marketplace_plugin_folder_missing`, `marketplace_plugin_empty` | The plugin's folder isn't there, or it has no `plugin.json` and the entry gives it nothing to load. |
| `marketplace_plugin_manifest_conflict` | `"strict": false` with components, over a plugin that has its own `plugin.json`. Claude Code doesn't load it and Enconvo refuses to install it. |
| `marketplace_plugin_name_mismatch`, `marketplace_plugin_version_mismatch` (warning) | The entry's name or version differs from the plugin's `plugin.json`. |
| `marketplace_plugin_hooks_not_inline` (warning) | Only hooks written in the entry are applied. |
| `marketplace_plugin_headers_helper_not_strict` | An `archive` source with a `headersHelper` needs `"strict": false`, so the plugin's whole manifest is in the entry where people can review it. Claude Code refuses to run the helper otherwise. |
| `marketplace_plugin_headers_ignored` (warning) | `headers` and `headersHelper` apply only to an `archive` source, so on any other source Claude Code and Enconvo ignore them. On an archive, Enconvo doesn't send `headers` or run `headersHelper`, so a download that needs them fails in Enconvo. |
| `marketplace_plugin_archive_unpinned` (warning) | An `archive` source with a `headersHelper` has no `source.sha256`. Pin the digest so people install the archive you reviewed. |
| `marketplace_plugin_header_dropped` (warning) | A routing or identity header, such as `Host`, `Cookie`, `X-Forwarded-For` or `Proxy-Authorization`, which Claude Code drops when it downloads the archive. |
| `marketplace_plugin_listing_field_misplaced` (warning) | The entry has `icon`, `documentationUrl`, `supportUrl`, `privacyPolicyUrl` or `termsOfServiceUrl`, which belong in the plugin's `.claude-plugin/plugin.json`. `claude plugin validate` reports them as unknown in an entry. |
| `marketplace_plugin_manifest_missing` (warning) | A Codex marketplace plugin without `.codex-plugin/plugin.json`. |
| `marketplace_policy_missing`, `marketplace_policy_invalid`, `marketplace_category_missing` | A Codex entry needs `policy.installation` (`AVAILABLE`, `INSTALLED_BY_DEFAULT` or `NOT_AVAILABLE`), `policy.authentication` (`ON_INSTALL` or `ON_USE`) and a `category`. A missing one is a warning; a wrong value is an error. |

## Enconvo `package.json`

| Code | Severity | Meaning |
| - | - | - |
| `plugin_name_format` | error | `name` uses only letters, digits, dots, dashes and underscores. |
| `plugin_version_not_semver`, `plugin_min_app_version_not_semver` | error | `version` or `minAppVersion` isn't a version number like `1.2.0`. |
| `plugin_title_missing` | error | `title` isn't set. |
| `plugin_description_missing` | warning | `description` isn't set. |
| `screenshots_too_many` | error | More than 8 images in `metadata/`. |
| `screenshot_file_ignored` | warning | A file in `metadata/` isn't an image and is left out. |
| `image_file_unreadable` | error | You don't have permission to read `metadata/` or an image in it. |
| `store_link_not_https` | warning | A link in `interface` (`websiteURL`, `supportURL`, `privacyPolicyURL`, `termsOfServiceURL`) isn't an `https://` address without a user name or password, of up to 2,048 characters, so the store page leaves it out. |

## Review materials

These check the `review` object (in `package.json`, or `extensions["com.openai"].review` in the `plugin.json` OpenAI's portal imports; see [where they go](/extensions/publishing-guidelines#review-materials)) and, in that `plugin.json`, `extensions["com.openai"].publication`. See [Review materials](/extensions/publishing-guidelines#review-materials). A field set to `null` counts as left out, as it does in OpenAI's import, which keeps the value saved there.

| Code | Severity | Meaning |
| - | - | - |
| `review_wrong_type`, `review_test_cases_wrong_type`, `review_test_case_wrong_type` | error | `review`, `test_cases` or a case isn't an object. |
| `review_test_cases_too_many` | error | A list has more than 20 cases. |
| `review_test_case_description_missing`, `review_test_case_prompt_missing`, `review_test_case_expected_behavior_missing` | error | A case needs a `description` and `prompt`; a positive case also needs `expected_behavior`. |
| `review_test_case_text_wrong_type`, `review_test_case_text_empty`, `review_test_case_text_too_long` | error | A case's text isn't text, is empty, or is over 4,000 characters. |
| `review_test_case_file_attachment_urls_wrong_type`, `review_test_case_file_attachments_too_many` | error | `file_attachment_urls` isn't a list of addresses, or has more than 10. |
| `review_test_case_url_format`, `review_demo_recording_url_format` | error | An address isn't `https://`. |
| `review_commerce_wrong_type` | error | `commerce` isn't `true` or `false`. |
| `review_commerce_description_wrong_type`, `review_commerce_description_empty`, `review_commerce_description_too_long` | error | `commerce_description` isn't usable text of up to 4,000 characters. |
| `publication_release_notes_wrong_type` | error | `publication.release_notes` isn't text. |
| `review_test_credentials_in_package`, `review_reviewer_instructions_in_package` | error | Sign-in details would ship inside the package, which anyone can download. Pass them with `plugin publish --review-notes`, or in the Review details form of OpenAI's dashboard. |
| `review_test_cases_server_count` | portal | `review.test_cases` in `plugin.json` are for a plugin with exactly one MCP server. A server declared in both `mcp.json` and `.mcp.json` under the same name counts once. |
| `publication_wrong_type`, `publication_countries_wrong_type`, `publication_translations_wrong_type`, `publication_translation_wrong_type` | portal | `publication`, `countries`, `translations` or a translation isn't the right shape: `countries` is a list, `translations` an object keyed by locale such as `fr-FR`. |
| `publication_country_format` | portal | A country isn't a two-letter uppercase code like `US`. `[]` makes the plugin available everywhere. |
| `publication_translation_locale_empty` | portal | A translation's locale is empty. |
| `publication_translation_subtitle_*`, `publication_translation_description_*` (`_wrong_type`, `_empty`, `_character_unsupported`, `_too_long`) | portal | A translated subtitle is one line of up to 30 characters; a description is up to 4,000 characters and may have line breaks. Neither may be only spaces or hold tabs or other control characters. The limits count spaces at either end. |
| `review_test_cases_missing` | warning, only with `--openai` | A plugin with an MCP server or app has no review materials. The warning names the manifest they go in. |
| `review_manifest_ignored` | warning, only with `--openai` | The review materials are in a manifest OpenAI's portal doesn't import, such as `.claude-plugin/plugin.json` beside a `.codex-plugin/plugin.json`. The Enconvo store still takes them from there. |
| `review_positive_test_cases_count`, `review_negative_test_cases_count` | warning | OpenAI's final submission takes exactly 5 positive and 3 negative cases. Checked in `plugin.json` only. |
| `review_demo_recording_missing`, `publication_release_notes_missing` | warning | OpenAI's final submission needs a demo video and release notes. Checked in `plugin.json` only; a plugin with only skills needs no demo. |
| `publication_country_unknown` | warning | A country isn't an ISO 3166 code. The United Kingdom is `GB`, not `UK`. |
| `review_test_case_tools_triggered_missing` | warning | A positive case doesn't name the tools it should call. |
| `review_commerce_description_missing` | warning | `commerce` is `true` but nothing says what is sold. |

## 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`.

| Code | Severity | Meaning |
| - | - | - |
| `claude_format_normalized` | warning, only with `--openai` | The plugin has only `.claude-plugin/plugin.json`. The portal converts it to `.codex-plugin/plugin.json` and fills in the listing, such as the display name and short description, with its own defaults. To write the listing yourself, add a `.codex-plugin/plugin.json` with an `interface`. |
| `submission_description_required` | warning, only with `--openai` | `interface.longDescription` isn't set. The directory shows it on the plugin's page; it takes up to 4,000 characters and may have line breaks. |
| `plugin_category_defaulted` | warning, only with `--openai` | `interface.category` isn't set, so the directory lists the plugin under Other. |
| `plugin_listing_urls_missing` | warning, only with `--openai` | A plugin with an MCP server needs `websiteURL`, `supportURL`, `privacyPolicyURL` and `termsOfServiceURL` in `interface`. A plugin with only skills doesn't. |
| `screenshot_count_mismatch` | warning, only with `--openai` | A plugin with an MCP server takes one screenshot for each starter prompt (`interface.defaultPrompt`). |
| `screenshot_format_unsupported` | warning, only with `--openai` | A screenshot isn't a PNG or JPEG image. |
| `screenshot_dimensions_unsupported` | warning, only with `--openai` | A screenshot isn't exactly 706 pixels wide and 400 to 860 pixels tall. |

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, give `validate` 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.

```sh theme={null}
npx -p @enconvo/api enconvo plugin validate --openai --previous my-plugin-1.0.0.zip
npx -p @enconvo/api enconvo plugin pack --openai --previous my-plugin-1.0.0.zip
```

Only what changed counts; the previous release's own problems aren't reported. `validate --json` lists the skills and MCP servers it compares under `components`.

| Code | Severity | Meaning |
| - | - | - |
| `plugin_previous_unreadable` | error | The previous release isn't there, or it isn't a plugin; the message gives its first error. |
| `plugin_name_mismatch` | portal | The plugin's `name` changed. OpenAI's portal takes an update only under the published plugin's name, and Enconvo and Codex install a renamed plugin as another plugin. |
| `plugin_version_unchanged` | warning, also with `--openai` | The `version` is the previous release's. OpenAI's portal asks you to confirm before it reuses a published version, and Enconvo's store doesn't take one. |
| `plugin_id_changed` | warning | The previous release has an `id` (`extensions["com.openai"].id`, or `id` in `.codex-plugin/plugin.json`) and this version has another one or none. Managed plugin catalogs know the plugin by the id they gave it, so keep it. OpenAI's portal keeps its own identity for an uploaded ZIP. |
| `plugin_version_not_higher` | warning | The version is lower than the previous release's, or differs from it only in build metadata (`1.0.0+2`). Enconvo's store takes only a higher version. Checked when both are version numbers like `1.2.0`. |
| `mcp_server_added_to_skills_plugin` | portal | The previous release has no MCP server and this version adds one. OpenAI's portal takes an MCP server only in a plugin's first ZIP. Checked for a plugin with a `plugin.json`; Enconvo's store takes the change. |
| `mcp_server_url_changed` | portal | A remote MCP server's URL changed. OpenAI's update flow can't move a published server: ask OpenAI's support to change it, or keep the old URL. A URL with a variable, such as `${MCP_URL}`, isn't compared. Checked for a plugin with a `plugin.json`. |
| `plugin_components_removed` | warning | This version leaves out skills or MCP servers the previous release has, so the update removes them for everyone who installed it. |

## Codes only OpenAI's portal reports

OpenAI's portal has a few codes that `validate` doesn't report, because they repeat a rule `validate` already checks under another code, or because only OpenAI can check them.

| Code | Meaning |
| - | - |
| `submission_display_name_*`, `submission_subtitle_*`, `submission_description_too_long`, `submission_description_character_unsupported`, `plugin_capability_invalid` | The listing you edit at final submission breaks the same limits as `interface.displayName`, `interface.shortDescription`, `interface.longDescription` and `interface.capabilities`; see [Text fields](#text-fields). |
| `submission_developer_name_*`, `developer_name_defaulted` | The developer name comes from your verified OpenAI identity. |
| `manifest_normalized` | The portal saves the manifest it read as `.codex-plugin/plugin.json`, and asks you to confirm any field it changed. |
| `skill_frontmatter_adjusted` | The portal trimmed the spaces around a skill's `name` and `description` and collapsed the spaces inside them. Nothing to fix. |
| `mcp_configuration_excluded` | The portal took an MCP configuration out of a skills-only upload. |
| `archive_member_path_too_long`, `skill_bundle_too_large` | A path, or a skill's compressed files, are over a limit OpenAI doesn't publish; the portal's message gives the limit. |
| `archive_plugin_files_missing` | A skills-only ZIP has no plugin manifest or no valid skill. `validate` reports these as `plugin_manifest_missing` and the `skill_*` codes. |
| `scan_required` | The portal needs a successful, current scan of the production MCP server; rescan after the server changes. `plugin scan --openai` checks the tools beforehand. |
| `domain_verification_required` | Host the token the portal gives you at `/.well-known/openai-apps-challenge` on the MCP server's host or an allowed parent host, then select **Verify Domain**. `plugin scan --openai` says what those URLs answer, and `--challenge-token <token>` checks for the exact token first; see [Domain verification](#domain-verification). |
| `app_not_eligible` | A local or workspace package refers to an MCP server that isn't eligible or available. To list the plugin in the directory, submit it **With MCP** and give the MCP server directly. |
| `justification_required` | A tool annotation has no justification of its read-only, open-world or destructive behavior. The portal's error list still has this code, though its submission page no longer asks for one; if the portal reports it, give the justification there. |

## 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.

```sh theme={null}
npx -p @enconvo/api enconvo plugin scan [path]                # the plugin's folder
npx -p @enconvo/api enconvo plugin scan [path] --openai       # also what OpenAI's portal needs
npx -p @enconvo/api enconvo plugin scan [path] --timeout 60   # seconds to wait for each server; the default is 30
npx -p @enconvo/api enconvo plugin scan [path] --json         # the result as JSON
npx -p @enconvo/api enconvo plugin scan [path] --challenge-token <token>   # checks the token OpenAI's portal gave you
```

```text theme={null}
notes (mcp.json, stdio: node ./mcp/server.mjs): 2 tools
  search_notes  readOnlyHint=true destructiveHint=false openWorldHint=false
  delete_note  readOnlyHint=false destructiveHint=? openWorldHint=?
warning[annotations_required] mcp.json: MCP server "notes": 1 of 2 tools doesn't set every annotation: delete_note (destructiveHint, openWorldHint). OpenAI's final submission asks for readOnlyHint, destructiveHint and openWorldHint as true or false on every tool, and Enconvo uses them to decide which tools run without asking. (OpenAI's plugin portal)
notes@1.2.0: 1 MCP server, 2 tools, 1 warning.
```

With `--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.

| Code | Severity | Meaning |
| - | - | - |
| `annotations_required` | portal | A tool doesn't set `readOnlyHint`, `destructiveHint` and `openWorldHint` to `true` or `false`. OpenAI's final submission asks for all three on every tool, and Enconvo uses them to decide which tools run without asking. See [MCP tools](/extensions/publishing-guidelines#mcp-tools) for which value fits. |
| `annotations_conflicting` | warning | A tool is marked both `readOnlyHint: true` and `destructiveHint: true`. A read-only tool changes nothing, so one of them is wrong. |
| `tool_description_missing` | warning | A tool has no description. The model picks tools by what their descriptions say. |
| `tool_name_duplicate` | warning | The server lists two tools with the same name. |
| `tools_missing` | warning, only with `--openai` | The server lists no tools, so OpenAI's portal has nothing to scan or review. |
| `domain_challenge_missing` | warning, only with `--openai` | No URL where OpenAI's portal verifies the server's domain serves a token yet. Expected until the portal gives you one after the upload; see [Domain verification](#domain-verification). |
| `domain_challenge_format` | warning | The server's own challenge URL answers with a web page, JSON, several tokens or a redirect instead of the token alone, or the token has spaces or a line break around it. The portal asks for the exact token as plain text. |
| `domain_challenge_mismatch` | error, only with `--challenge-token` | No challenge URL serves the token you gave. The message lists what each one answers. |
| `domain_challenge_unchecked` | warning | You gave `--challenge-token`, but the plugin has no MCP server at a public `https://` URL, so there's no domain to verify. |
| `mcp_server_not_remote` | warning, only with `--openai` | The server runs on the user's computer, or its URL isn't a public `https://` address. OpenAI's portal connects to a server at a public `https://` URL to scan and review it, so deploy the server and declare its URL before you submit there. Enconvo, Codex and Claude Code run a local server as it is. |
| `scan_server_failed` | error | The server couldn't start, exited, or didn't answer in time. The message quotes the end of what it printed on stderr. When a command uses paths like `./server.js`, start it from the plugin's folder: `${CLAUDE_PLUGIN_ROOT}/server.js` in `.mcp.json`, or `"cwd": "./"` in `mcp.json`. |
| `scan_sign_in_required` | warning | The server asked for sign-in (HTTP 401 or 403), so its tools weren't listed. Enconvo signs in when the plugin is installed. To scan it here, give the server a token through `bearer_token_env_var` or its `headers`. |
| `scan_stdout_not_jsonrpc` | warning | A command server printed something on stdout that isn't an MCP message. Enconvo, Codex and Claude Code read only JSON-RPC there, so log to stderr. |
| `scan_variable_unset` | warning | The server's settings use a variable, such as `${API_KEY}`, that isn't set in your shell. Set it to scan the server as it will run. |
| `scan_transport_unsupported` | warning | The server uses the older `sse` transport, or another one the scan doesn't speak, so it wasn't scanned. Enconvo still runs an `sse` server. |
| `ui_template_openai_only` | warning | A tool names its view only in `_meta["openai/outputTemplate"]`. Enconvo and ChatGPT read that key, but other MCP Apps hosts read only `_meta.ui.resourceUri`, so set that to the same `ui://` URI as well. |
| `ui_resource_unreadable` | warning | A tool points at a view the server doesn't give: the server doesn't offer resources, or `resources/read` fails for that URI. Enconvo and other hosts then show the tool's result without its interface. |
| `widget_csp_openai_only` | warning | A view declares domains only in `_meta["openai/widgetCSP"]` (`connect_domains`, `resource_domains`, `frame_domains`). Enconvo and ChatGPT read that key, but other MCP Apps hosts read only `_meta.ui.csp` (`connectDomains`, `resourceDomains`, `frameDomains`), so the view can't reach those domains there. Declare them in both. |
| `frame_domain_explanation_required` | warning, only with `--openai` | A view embeds pages from other domains (`frameDomains`). OpenAI's final submission asks you to explain each one: what the page does, why the plugin embeds it, and who controls its domain. Pages from the MCP server's own registrable domain are allowed; separate tenants on a shared host count as different domains. |
| `screenshots_not_allowed` | error, only with `--openai` | `interface.screenshots` lists images, but no tool shows an interface of its own (`_meta.ui.resourceUri` or `_meta["openai/outputTemplate"]`). OpenAI's portal takes screenshots only from a plugin whose scan finds one, and its directory shows starter prompts instead. Reported only when every server was scanned. |
| `scan_servers_missing` | error | The plugin declares no MCP server. |
| `scan_archive_unsupported` | error | `scan` was given a `.zip`. |

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**:

```sh theme={null}
npx -p @enconvo/api enconvo plugin scan --challenge-token tok_123
```

```text theme={null}
domain verification:
  https://mcp.example.com/.well-known/openai-apps-challenge: HTTP 404
  https://example.com/.well-known/openai-apps-challenge: the token you gave
```

One URL that serves the token is enough. `scan` exits with status 1 when none does (`domain_challenge_mismatch`).

## Packing and publishing

| Code | Severity | Meaning |
| - | - | - |
| `plugin_path_missing` | error | The folder doesn't exist. |
| `plugin_build_missing` | error | Build the Enconvo plugin first (`npm run build`). |
| `plugin_format_not_packable` | error | A desktop extension is packed with `npx @anthropic-ai/mcpb pack`. |
| `plugin_format_not_publishable` | error | The store takes Enconvo, Codex, Claude Code and portable plugins; share a desktop extension's `.mcpb` file instead. |
| `plugin_version_missing` | error | The store needs a version. |
| `plugin_package_too_large` | error | The package is over 50 MB. |
| `plugin_readme_too_large` | warning | The README is over 64 KB, so the store page leaves it out. |
| `store_icon_missing` | warning | The plugin has no icon the store can show (PNG, JPEG, WebP or GIF up to 5 MB) in `interface.logo` or the other `interface` logos, or in a Claude Code plugin's `icon`, so it shows a default icon. |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.