You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
apm install refuses any Claude Code plugin whose .claude-plugin/plugin.json carries the official Claude Code $schema. The manifest is valid for Claude Code, and the identical package installs cleanly once the $schema line is deleted.
The rejection comes from a root-manifest router being reused as a predicate on the legacy manifest, where it raises instead of returning.
[x] Failed to install APM dependencies: Local package is invalid: Failed to
process Claude plugin: Unsupported schema-bearing plugin manifest:
https://json.schemastore.org/claude-code-plugin-manifest.json. APM accepts
schema-bearing plugin.json only with
https://agent-plugins.org/schemas/1.0.0/plugin.schema.json; remove $schema only
for a schema-less legacy plugin.
Expected: it installs as a legacy Claude plugin, which is what happens when $schema is absent.
Also reproduces over the network against a real package: apm install blader/humanizer. --force does not bypass it.
Row 2 is the error the call site is actually reaching for. Row 1 is the same call raising before it can answer. Row 3 is what I expected row 1 to do.
Root cause
flowchart TD
A["apm install<br/>package with .claude-plugin/plugin.json"] --> B["detect_package_type<br/>MARKETPLACE_PLUGIN"]
B --> C["_validate_marketplace_plugin<br/>validation.py:776"]
C --> D["normalize_plugin_directory<br/>plugin_parser.py:456<br/>plugin.json = optional metadata"]
D --> E{"classify_plugin_manifest_schema<br/>local_bundle.py:79"}
E -->|"no $schema"| F["return LEGACY<br/>line 82"]
E -->|"agent-plugins.org"| G["return AGENT_PLUGIN"]
E -->|"any other $schema"| H["raise AgentPluginManifestError<br/>line 94"]
F --> I["plugin installs"]
G --> J["raise AgentPluginLegacyBoundaryError<br/>must be admitted from the package root"]
H --> K["caught at validation.py:806<br/>Failed to process Claude plugin"]
K --> L["install fails"]
style H fill:#ffe0e0
style L fill:#ffe0e0
Loading
classify_plugin_manifest_schema is scoped to root manifests by its own definition. local_bundle.py:72-73 reads: "Admission route selected exclusively by root plugin.json." It returns LEGACY when $schema is absent (line 82) and raises for any $schema it does not recognise (line 94).
normalize_plugin_directory handles the legacy .claude-plugin/ layout, where plugin.json is "treated as optional metadata" (plugin_parser.py:460). At line 489 it calls the router for one purpose only, to reject an Agent Plugins manifest placed outside the package root:
ifclassify_plugin_manifest_schema(manifest) isPluginSchemaRoute.AGENT_PLUGIN:
raiseAgentPluginLegacyBoundaryError(
"Schema-bearing plugin.json must be admitted from the package root, ""not Claude plugin normalization"
)
With a Claude Code $schema the call raises, so the comparison never evaluates and the legacy path is unreachable.
Suggested fix
Treat an unrecognised $schema as LEGACY at this call site, either by catching AgentPluginManifestError around plugin_parser.py:489 or by giving the router a non-raising variant for legacy admission. Strictness for a root plugin.json is unaffected.
Happy to send a PR if you want the fix in that shape. Given the persona-review contract in CONTRIBUTING, I would rather confirm the approach with you first than guess at it.
Why this shape appears in the wild
$schema is a generic JSON Schema pointer, and Claude Code plugins normatively carry the schemastore URL. This project's own CHANGELOG entry for #1061 cites that schema when describing what plugin.json array entries must contain. Any marketplace shipping schema-conformant Claude plugins is affected, and the remedy in the error text asks authors to stop conforming to their own ecosystem's schema.
Summary
apm installrefuses any Claude Code plugin whose.claude-plugin/plugin.jsoncarries the official Claude Code$schema. The manifest is valid for Claude Code, and the identical package installs cleanly once the$schemaline is deleted.The rejection comes from a root-manifest router being reused as a predicate on the legacy manifest, where it raises instead of returning.
Reproduction
Three files, no network:
Actual:
Expected: it installs as a legacy Claude plugin, which is what happens when
$schemais absent.Also reproduces over the network against a real package:
apm install blader/humanizer.--forcedoes not bypass it.What varies
plugin.jsonlocation$schema.claude-plugin/Unsupported schema-bearing plugin manifest.claude-plugin/must be admitted from the package root.claude-plugin/plugin.jsonUnsupported schema-bearing plugin manifest, raised earlierRow 2 is the error the call site is actually reaching for. Row 1 is the same call raising before it can answer. Row 3 is what I expected row 1 to do.
Root cause
flowchart TD A["apm install<br/>package with .claude-plugin/plugin.json"] --> B["detect_package_type<br/>MARKETPLACE_PLUGIN"] B --> C["_validate_marketplace_plugin<br/>validation.py:776"] C --> D["normalize_plugin_directory<br/>plugin_parser.py:456<br/>plugin.json = optional metadata"] D --> E{"classify_plugin_manifest_schema<br/>local_bundle.py:79"} E -->|"no $schema"| F["return LEGACY<br/>line 82"] E -->|"agent-plugins.org"| G["return AGENT_PLUGIN"] E -->|"any other $schema"| H["raise AgentPluginManifestError<br/>line 94"] F --> I["plugin installs"] G --> J["raise AgentPluginLegacyBoundaryError<br/>must be admitted from the package root"] H --> K["caught at validation.py:806<br/>Failed to process Claude plugin"] K --> L["install fails"] style H fill:#ffe0e0 style L fill:#ffe0e0classify_plugin_manifest_schemais scoped to root manifests by its own definition.local_bundle.py:72-73reads: "Admission route selected exclusively by root plugin.json." It returnsLEGACYwhen$schemais absent (line 82) and raises for any$schemait does not recognise (line 94).normalize_plugin_directoryhandles the legacy.claude-plugin/layout, where plugin.json is "treated as optional metadata" (plugin_parser.py:460). At line 489 it calls the router for one purpose only, to reject an Agent Plugins manifest placed outside the package root:With a Claude Code
$schemathe call raises, so the comparison never evaluates and the legacy path is unreachable.Suggested fix
Treat an unrecognised
$schemaasLEGACYat this call site, either by catchingAgentPluginManifestErroraroundplugin_parser.py:489or by giving the router a non-raising variant for legacy admission. Strictness for a rootplugin.jsonis unaffected.Happy to send a PR if you want the fix in that shape. Given the persona-review contract in CONTRIBUTING, I would rather confirm the approach with you first than guess at it.
Why this shape appears in the wild
$schemais a generic JSON Schema pointer, and Claude Code plugins normatively carry the schemastore URL. This project's own CHANGELOG entry for #1061 cites that schema when describing whatplugin.jsonarray entries must contain. Any marketplace shipping schema-conformant Claude plugins is affected, and the remedy in the error text asks authors to stop conforming to their own ecosystem's schema.Related
"skills": ["./"]in plugin.json crashes install with "maximum recursion depth exceeded" #2556: same package, different gate, fixed in 0.29.0. I verified that fix works: with$schemaremoved and"skills": ["./"]kept,blader/humanizerinstalls cleanly on 0.29.0. This report is the remaining barrier for that package.apm.ymland a rootplugin.jsoncannot be installed as an APM package #2735 and fix: prefer APM manifests over plugins (closes #2735) #2776 also concern package-type precedence, but they apply when a rootapm.ymlis present. This package has none.Environment
apm 0.29.0 (
5ac6733), macOS arm64. Line references are against v0.29.0.