Skip to content

Recognize .claude-plugin/plugin.json for external plugin submissions 🤖🤖🤖 - #2524

Open
conorbronsdon wants to merge 2 commits into
github:mainfrom
conorbronsdon:fix/external-plugin-claude-plugin-manifest
Open

Recognize .claude-plugin/plugin.json for external plugin submissions 🤖🤖🤖#2524
conorbronsdon wants to merge 2 commits into
github:mainfrom
conorbronsdon:fix/external-plugin-claude-plugin-manifest

Conversation

@conorbronsdon

Copy link
Copy Markdown

Pull Request Checklist

  • I have read and followed the CONTRIBUTING.md guidelines.
  • I have read and followed the Guidance for submissions involving paid services.
  • My contribution adds a new instruction, prompt, agent, skill, workflow, or canvas extension file in the correct directory.
  • The file follows the required naming convention.
  • The content is clearly structured and follows the example format.
  • I have tested my instructions, prompt, agent, skill, workflow, or canvas extension with GitHub Copilot.
  • I have run npm start and verified that README.md is up to date.
  • I am targeting the main branch for this pull request.

Two boxes are unchecked because they do not apply: this is a fix to the external plugin intake scripts in eng/, not a new resource file.


Description

External plugin intake resolves a submission's manifest from a fixed list of three locations: .github/plugin/plugin.json, .plugin/plugin.json, and plugin.json. A plugin whose manifest sits at .claude-plugin/plugin.json fails the version match gate with No plugin.json found, and the submitter gets requires-submitter-fixes before a maintainer looks at it.

This adds .claude-plugin/plugin.json as a fourth candidate.

Why this location

The Copilot CLI already searches it. The manifest resolver in the shipped native runtime carries the candidate directory set root, .plugin, .github/plugin, .claude-plugin, plus the matching set for marketplace.json:

$ npm pack @github/copilot-linux-x64@1.0.77 && tar xzf github-copilot-linux-x64-1.0.77.tgz
$ grep -a -o -E ".{45}claude-plugin.{25}" package/prebuilds/linux-x64/runtime.node | sort -u
ketplace.json.github/plugin/marketplace.json.claude-plugin/marketplace.jsonMarketpl
lugin.schema.json..--/.plugin..github/plugin.claude-pluginplugin.jsoncache_pathexte
rsiondirentriessources.plugin..github/plugin.claude-pluginplugin.jsonversionauthorr
stdoutstderrresolvedUrl.plugin.github/plugin.claude-pluginplugin.jsonsuccessFailed

The intake list is a subset of what the CLI installs from, so the gate currently rejects plugins that would install correctly.

This is not hypothetical for the existing catalog. Probing all 37 entries in plugins/external.json at their pinned ref/sha, 15 ship a .claude-plugin/plugin.json. For microsoft/skills-for-copilot-studio it is the only manifest in the repository, so its manifest is unresolvable under the current list.

Why last in the list

Resolution is first-match-wins, so appending cannot change any submission that resolves today. netlify/context-and-tools is the entry that makes this concrete. It ships two manifests with different names and versions:

Path name version
.github/plugin/plugin.json netlify 1.1.0
.claude-plugin/plugin.json netlify-skills 1.2.0

external.json pins 1.1.0. Ordering .claude-plugin first would flip that listed entry from pass to fail; ordering it last keeps it passing. There is a test for exactly this.

Scope

The list exists in three places, which I updated together:

File What it drives
external-plugin-quality-gates.mjs PLUGIN_JSON_CANDIDATES, manifest resolution for the quality gates
external-plugin-intake.mjs canvas plugin metadata resolution
external-plugin-validation.mjs the expected-location text in the error message only

Two secondary effects, both widening .claude-plugin to match how the other three locations are already handled: intake resolves canvas metadata from the new location, and buildVallyLintArgs lints the directories named in a .claude-plugin manifest's skills array instead of falling back to the whole plugin root.

I could not run the install smoke test, because it shells out to the copilot CLI and that is not available in my environment. Reading runInstallSmokeGate, its post-install assertion calls the same findPluginJson, so it widens consistently with the resolver.


Type of Contribution

  • New instruction file.
  • New prompt file.
  • New agent file.
  • New plugin.
  • New skill file.
  • New agentic workflow.
  • New canvas extension.
  • Update to existing instruction, prompt, agent, plugin, skill, workflow, or canvas extension.
  • Other (please specify): bug fix in eng/ external plugin intake and quality gate scripts.

Additional Notes

Verification

Reproduction of the bug against a real repository, using runVersionMatchGate from this repo unmodified:

STATUS: fail
OUTPUT: - v3.22.2: No plugin.json found at "v3.22.2". Expected one of:
  plugins/avoid-ai-writing/.github/plugin/plugin.json,
  plugins/avoid-ai-writing/.plugin/plugin.json,
  plugins/avoid-ai-writing/plugin.json

Same call with this branch applied:

STATUS: pass
OUTPUT: - v3.22.2: matched version "3.22.2" at "plugins/avoid-ai-writing/.claude-plugin/plugin.json".
- ae1ecbebf09f45b67d2cd1e1c902c4b240e770fc: matched version "3.22.2" at "plugins/avoid-ai-writing/.claude-plugin/plugin.json".

Test suites:

$ node --test eng/external-plugin-quality-gates.test.mjs
# tests 15
# pass 15
# fail 0

$ node --test eng/external-plugin-intake.test.mjs        # pass 12  # fail 0
$ node --test eng/external-plugin-validation.test.mjs    # pass 13  # fail 0
$ node --test eng/materialize-plugins.test.mjs           # pass 2   # fail 0

The three new tests were mutation-checked by reverting only the source change and re-running:

not ok 13 - runVersionMatchGate resolves a manifest at .claude-plugin/plugin.json
not ok 14 - runVersionMatchGate still fails when no manifest exists in any known location
ok 15 - runVersionMatchGate prefers .github/plugin/plugin.json over .claude-plugin/plugin.json
# pass 13
# fail 2

Test 13 covers the fix. Test 14 is a no-regression guard whose fix-sensitive assertion is the error message enumerating all four locations. Test 15 passes on both, as a guard should. Moving .claude-plugin to the front of the list fails it, which is what pins the ordering.

Repository toolchain:

$ npm run plugin:validate
✅ external.json is valid (37 external plugins)
✅ All 71 plugins, 21 extensions, and the external catalog are valid
🎉 Plugin validation passed

$ npm run skill:validate
✅ All 395 skills are valid
🎉 Skill validation passed

$ npm run build
✓ Successfully generated marketplace.json with 129 plugins (92 local, 37 external)

$ bash eng/fix-line-endings.sh
Done! All markdown files now have LF line endings.

$ git status --short
(no output: build and line-ending normalization produce no changes)

All four changed files are LF, verified with git cat-file blob <sha> | tr -dc '\r' | wc -c returning 0.

Follow-up worth a separate issue

The gate's manifest precedence is not verified to match the CLI's. For netlify the gate validates netlify 1.1.0 from .github/plugin/, while the CLI may load netlify-skills 1.2.0 from .claude-plugin/. That divergence predates this PR, since it is already possible between .plugin and the repository root, so I left it alone rather than widen the change.

I am happy to drop the external-plugin-intake.mjs and external-plugin-validation.mjs changes if you would rather keep this to the quality gate alone, though the in-repo comments ask for the three lists to be kept in sync.

🤖 Generated with Claude Code

External plugin intake and quality gates resolve a submission's manifest from a
fixed list of three locations: .github/plugin/plugin.json, .plugin/plugin.json,
and plugin.json. A plugin whose manifest sits at .claude-plugin/plugin.json is
reported as "No plugin.json found" and fails the version match gate before a
maintainer sees it.

The Copilot CLI already searches that location. The manifest resolver in the
shipped native runtime (@github/copilot-linux-x64) carries the candidate
directory set root, .plugin, .github/plugin, .claude-plugin, and the equivalent
set for marketplace.json. So the intake list is a subset of what the CLI itself
installs from, and the gap rejects plugins that would install correctly.

Fifteen of the 37 entries already in plugins/external.json ship a
.claude-plugin/plugin.json. For microsoft/skills-for-copilot-studio it is the
only manifest in the repository, so its manifest is unresolvable under the
current list.

Add the location as a fourth candidate in all three copies of the list, ordered
last so a repository shipping any of the original three keeps resolving to that
one. netlify/context-and-tools is the case that makes ordering matter: it ships
name=netlify version=1.1.0 at .github/plugin/plugin.json and name=netlify-skills
version=1.2.0 at .claude-plugin/plugin.json, and external.json pins 1.1.0.

Two secondary effects, both widening to match how the other three locations are
already treated. Intake now resolves canvas metadata from the new location, and
buildVallyLintArgs now lints the directories named in a .claude-plugin manifest's
skills array rather than falling back to the whole plugin root.

Tests cover the new location, the unchanged failure when no manifest exists in
any location, and the precedence of .github/plugin/plugin.json when both are
present.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

🔴 Contributor Reputation Check: HIGH risk

Check Risk
Profile HIGH
Credential audit NONE

Maintainers: please review this contributor before merging.
See the workflow run for full details.
Automated check powered by AGT.

@github-actions github-actions Bot added the needs-review:HIGH Contributor reputation check flagged HIGH risk label Aug 3, 2026
The comment said `.claude-plugin/plugin.json` is "the Claude Code plugin
spec location that CONTRIBUTING.md points external submitters at."
CONTRIBUTING.md does not mention `.claude-plugin` at all -- `grep -c`
returns 0. It points submitters at `.github/plugin/plugin.json` (lines
145 and 175); what it borrows from the Claude Code spec is the field
vocabulary (`agents`, `commands`, `skills`), not the directory.

The actual justification is the one in the PR description and it did not
travel into the code: the Copilot CLI's own manifest resolver searches
this directory, so a plugin the CLI can install should resolve here too.
Stating that instead, since it is the claim the change rests on and it is
the one that survives a maintainer checking it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@conorbronsdon
conorbronsdon marked this pull request as ready for review August 3, 2026 17:51
@aaronpowell aaronpowell self-assigned this Aug 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-review:HIGH Contributor reputation check flagged HIGH risk

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants