A Codex skill file is not a Muse skill until the manifest names the directory

John Burns

The skill file was already on disk. skills/greet/SKILL.md held a name, a description, and one sentence of instructions. .codex-plugin/plugin.json named the plugin demo-plugin, set version 0.1.0, and set a description. muse plugins validate still exited 1. The diagnostic code was invalid-manifest-schema. The message was one sentence:

this Codex plugin declares no importable skills yet (hooks/MCP/commands are not imported in this phase)

That sentence also printed when the tree had no skills/ directory, and when the manifest pointed at hooks/hooks.json instead of a skill. A script that treats it as “the skill file is missing” will look in the wrong place. On this binary the sentence means the Codex overlay did not declare a skill directory Muse will import. The parenthetical is a phase limit. Hooks, MCP servers, and commands are not imported yet. They are not a substitute for the missing string, and they do not change the wording when the skill file is sitting next to the manifest.

What this run used

The binary was Muse Code 1.4.14, release 1.4.14-R7309.1, on Linux x86_64, on 9 November 2026. The executable was the versioned binary beside the launcher. Its SHA-256 was 0809aac7cb58d18f5022b6d05e02aa1974683c0ec430e5f04d25b60ed17de2da. muse --version printed Muse Code 1.4.14 (1.4.14-R7309.1). MUSE_NO_AUTO_UPDATE=1 was set so the launcher’s update check was not part of the result. Each install used a new XDG config home and a new data home. The operator Muse home was not the directory under test. No muse login was run. No prompt was sent. Nothing was installed over the Muse checkout.

The comparison command was claude plugin validate from Claude Code 2.1.308, commit 320f66eb8f83, on the same host. CLAUDE_CONFIG_DIR pointed at a new directory for those calls. No Claude login was run.

The Codex packaging page, read the same day, describes two layouts. The plugin-creator scaffold writes .codex-plugin/plugin.json and, when skills are requested, declares skills: "./skills/". The portable layout puts plugin.json at the plugin root with $schema set to https://agent-plugins.org/schemas/1.0.0/plugin.schema.json. That page says portable packages discover skills from the root skills/ directory, so the manifest does not need a skills field. It also calls .codex-plugin/plugin.json an optional compatibility fallback. This run checked those two sentences against the installed Muse binary. It did not run Codex’s installer, and it does not say what a current Codex process will accept.

The overlay does not scan the directory

A Claude-shaped tree does scan. The same minimal JSON, stored at .claude-plugin/plugin.json, with skills/greet/SKILL.md beside it and no skills key, validated as claude-compatible with skills=1 and exited 0. Install then exited 0. muse skills list --source plugin listed plugin:demo-plugin:greet. The description was “Say hello from the probe skill.” The install record said provenance was foreign-import and manifest_family was claude-compatible.

Move that JSON into .codex-plugin/plugin.json and leave the skill file where it is. Validate exits 1 and prints the sentence above. The skill file is not counted. The family column never appears, because the text line starts with invalid and the JSON sets plugin to null.

Three other spellings of “the skills are here” also failed, and they failed differently.

An array is a type error. "skills": ["./skills/"] exited 1 with public Codex manifest skills must be a string path. A file path is a shape error. "skills": "./skills/greet/SKILL.md" exited 1 with public Codex skills path must be a directory. An interface object that held a skills path was ignored. That tree fell back to the same “no importable skills yet” sentence, which is the wording you also get when the key is absent. If you are reading the error to decide whether the key is missing or the wrong type, the absent key and the ignored interface key look the same. The array and the file path do not.

The string that works is a directory path. This manifest validated, exited 0, and printed valid, demo-plugin, codex-compatible, skills=1:

{
  "name": "demo-plugin",
  "version": "0.1.0",
  "description": "string skill path",
  "skills": "./skills/"
}

--json set compatibility.summary to full and classified skill:greet as supported. enabled_default on that skill was true. The Claude import stored enabled_default as null and still listed the skill with activation on after install. Do not treat a null default as a disabled skill. Read activation on the skill list, or the skill id itself.

Install of that Codex tree exited 0. The record again said foreign-import, this time with manifest_family codex-compatible. skills list --source plugin showed plugin:demo-plugin:greet. It also showed plugin:threejs:threejs on a home that had never installed a plugin. That row ships with this binary. Count the skill id you installed. Do not treat the length of the plugin-scoped list as the size of the package.

A portable root manifest does scan

The other sentence on the Codex page held, and only for the portable file. A root plugin.json with the exact Agent Plugins 1.0.0 $schema, a name, a version, a description, and no skills field, plus skills/greet/SKILL.md, validated as agent-plugins with skills=1 and compatibility.summary full. Install exited 0. The family was agent-plugins, not codex-compatible. Provenance was again foreign-import. enabled_default was true.

That scan is not a courtesy for any file named plugin.json at the root. A root file whose $schema was a different URL, with the same skill directory beside it, exited 1. The warning was ignored-root-manifest. The message said the file declares an unknown root format, is not an Agent Plugins marker, and is ignored for format selection. The error was missing-manifest. The skill directory did not become a plugin by itself once Muse had refused the only manifest.

The schema document at that URL, fetched the same day, requires $schema and name, and sets additionalProperties to false. The $schema const is the URL itself. Muse’s message matches the const: the marker has to be exact. A root plugin.json with name, version, and description, and no $schema at all, is also missing-manifest. It is not a partial portable package that still scans skills/.

A root file with the exact schema and no skill directory is a third result, and it is the one that will fool a status check. Validate exits 0. The family is agent-plugins. The skill count is 0. --json sets compatibility.summary to unsupported. Install then exits 1:

plugin compatibility is unsupported: no supported behavior capabilities

The JSON error code for that empty portable package was incompatible-plugin-package. The same message, and the same code, is what install printed for a Claude overlay that had validated with skills=0. A native .muse-plugin/plugin.json that declared schemaVersion 1, a description, compat.manifestDir, and an empty capabilities object also validated. Its family was native, its skill count was 0, and its summary was unsupported. Install refused it with the same message under a different code, invalid-plugin-package. Exit 0 from validate is not the install gate. The summary is.

The text line can say valid while the JSON already says the package has nothing this binary will install. If a wrapper only scrapes the first field, it will try to install a package Muse has already classified as unsupported.

Claude’s validator is a different gate

claude plugin validate on the Codex tree that held skills/greet/SKILL.md and no skills string exited 0 and printed Validation passed. The directory note said there was no .claude-plugin/plugin.json, and that a skills tree loads without a manifest. Claude Code did not read the Codex overlay’s missing skills field. A pass from that command does not mean Muse will import the overlay.

On the portable root tree that Muse installed, the same Claude command also exited 0. On a root plugin.json with the exact schema, no .claude-plugin/ directory, and no skill file, Claude exited 1 and said no manifest was found. It expected .claude-plugin/marketplace.json or .claude-plugin/plugin.json. Muse had accepted that empty root file as agent-plugins and then refused it at install. The two commands can disagree in either direction. Use Muse’s validate and Muse’s install against the tree you intend to install. Do not substitute Claude’s exit code.

A symlink under skills/ is the case where Claude’s pass is the more permissive one. Muse exited 1 with unsafe-path. The message said Claude skill sources must contain only regular files and directories. A second diagnostic, a warning, said the package contains symlink entries and cannot be installed. Claude exited 0. It warned that one entry was a symlink and was not read, and it said a session loading the plugin does follow symlinks. If you are installing into Muse, replace the link with a regular file before validate. A Claude warning is not a Muse install.

The Claude plugin reference, read the same day, says a manifest is optional and that Claude Code loads the components it finds in the standard layout, including skills/. It also says Claude Code ignores $schema at load time. That matches the Claude results here. It does not describe Muse’s family column, and it does not describe the install refusal that follows an unsupported summary.

Two manifests do not fail the exit code

A tree with both .claude-plugin/plugin.json and .codex-plugin/plugin.json, and no skill files, exited 0. The text line was valid, demo-plugin, claude-compatible, skills=0, diagnostics=1. The diagnostic was a warning, multiple-manifests: selected .claude-plugin/plugin.json, ignoring .codex-plugin/plugin.json. The Codex file was not merged. It was dropped. A checker that fails only on a non-zero status will not see that drop.

A root Agent Plugins file named root-plugin, plus a nested .claude-plugin/plugin.json named demo-plugin, also exited 0. Muse selected the root name and the family agent-plugins. The warning was agent-overlay-inactive. The legacy overlay does not carry the Agent root name, and the overlay contributes nothing. Claude’s validator on that tree read the nested file and passed with an author warning. The name you will install is the name Muse printed, not the name in the overlay Claude read.

That empty dual-manifest tree is still not installable. A skill count of 0, and a summary of unsupported on the selected package, predict the install refusal above. Read the warning, then read the summary, then install.

Verify the skill id after install

Point muse plugins validate at the plugin root, not at the nested directory. Add --json when a script will decide. The text line is enough for a person if you read every field, not only valid and the exit code.

For a Codex overlay, open .codex-plugin/plugin.json and require skills to be a string whose value is a directory under the plugin root, such as ./skills/. Confirm skills/<name>/SKILL.md is a regular file. Validate should print codex-compatible and a skill count greater than zero. --json should show compatibility.summary of full before you install.

For a portable package, require the exact $schema URL, keep plugin.json at the root, and put each skill under skills/<name>/SKILL.md. Validate should print agent-plugins and a non-zero skill count. A different $schema is ignored. The skill directory will not save it.

For a Claude overlay, a missing skills key is not the defect. Muse scans skills/. A zero count means the scan found nothing it will install. Fix the files, or expect install to refuse with no supported behavior capabilities.

After a successful install, run muse skills list --source plugin and look for plugin:<plugin-id>:<skill-id>. Ignore bundled rows that were already present on an empty home. This run did not start a model session, so the list is the evidence that the skill was registered. It is not evidence that a later turn will call it.

Hooks, MCP servers, and commands in a Codex overlay are outside this phase. A hooks file does not change the “no importable skills” sentence. If the package you care about is a hook and not a skill, this binary will not import it through plugins validate and plugins install. That is a limit of 1.4.14, not a missing file.

plugins validate exit 0 means Muse selected a manifest and did not record an error diagnostic. A warning still leaves the exit at 0. plugins install exit 0 means the package was copied into the data home and marked enabled. The compatibility summary is the field that connects those two. full, with a supported skill, installed. unsupported, with an empty capability list, did not, even when validate had already printed valid.

The Codex packaging page is right about the scaffold string, and right about the portable scan, and easy to mis-apply at the boundary between them. The sentence that says a portable manifest does not need a skills field does not apply to .codex-plugin/plugin.json on this Muse build. Copy the scaffold’s skills string into the overlay, or move the package to a root manifest with the exact schema and let the scan run. Then install, and read the skill id back.

Sources

  • Muse Code 1.4.14 (1.4.14-R7309.1), binary SHA-256 0809aac7cb58d18f5022b6d05e02aa1974683c0ec430e5f04d25b60ed17de2da, Linux x86_64, 9 November 2026. The commands were plugins validate, plugins install --scope user, plugins list, plugins inspect, and skills list --source plugin, each against a disposable XDG home. No login and no model turn.
  • Claude Code 2.1.308, claude plugin validate on the same trees, with an isolated config directory and no login.
  • OpenAI, “Package your plugin,” https://developers.openai.com/codex/plugins/build, read 9 November 2026. The scaffold declares skills: "./skills/". Portable packages discover skills/ without that field.
  • Agent Plugins schema, https://agent-plugins.org/schemas/1.0.0/plugin.schema.json, fetched 9 November 2026. $schema and name are required, and the schema const is that URL.
  • Claude Code plugin manifest reference, https://code.claude.com/docs/en/plugins-reference, read 9 November 2026. A skills tree loads without a manifest, and $schema is ignored at load time.