Skip to content

fix: make root marketplace catalog real files (not symlinks) - #395

Open
jpelletier1 wants to merge 3 commits into
mainfrom
openhands/fix-symlinked-marketplace-catalog
Open

fix: make root marketplace catalog real files (not symlinks)#395
jpelletier1 wants to merge 3 commits into
mainfrom
openhands/fix-symlinked-marketplace-catalog

Conversation

@jpelletier1

@jpelletier1 jpelletier1 commented Jul 8, 2026

Copy link
Copy Markdown
Contributor
  • A human has tested these changes.

Why

When the plugin-directory is configured with MARKETPLACE_SOURCE=github://openhands/extensions, the marketplace catalog does not render (empty Plugin Directory).

Root cause: the catalog was exposed through a double symlink chain, and raw.githubusercontent.com does not dereference git symlinks (it serves the literal symlink target text; for symlinked directory paths it returns 404):

  • .claude-plugin (symlink) -> .plugin
  • .plugin/marketplace.json (symlink) -> ../marketplaces/openhands-extensions.json

Verified empirically against main: GET .plugin/marketplace.json returns HTTP 200 but the body is the literal string ../marketplaces/openhands-extensions.json (not JSON), and GET .claude-plugin/marketplace.json returns 404 (symlinked directory not traversable). So consumers doing GET + json.loads() get a 404 or a parse error and an empty catalog. This is a server-side (GitHub raw) limitation the plugin-directory client cannot work around, so the catalog simply never arrives.

Summary

  • Replaced the root vendor symlinks (.claude-plugin, .codex-plugin, .plugin/marketplace.json) with real directories + real marketplace.json files that each mirror the canonical marketplaces/openhands-extensions.json byte-for-byte (canonicalized JSON). Git mode changed from 120000 (symlink) to 100644 (regular file). The catalog is now reachable at every standard .claude-plugin/marketplace.json / .codex-plugin/marketplace.json / .plugin/marketplace.json path over the raw URL.
  • Added a root-marketplace sync task to scripts/sync_extensions.py that keeps all three root copies in sync with the canonical file and reports drift in --check mode. Since CI runs python scripts/sync_extensions.py --check with no task filter, the new task is automatically enforced on every PR.
  • Added tests in tests/test_sync_extensions.py (TestRootMarketplace) verifying the real-repo files are non-symlinks, match the canonical catalog, are valid JSON, and that the sync task detects/fixes both the original symlink chain and stale copies.
  • Updated AGENTS.md with the invariant (root catalog copies are real files, never symlinks).

Issue Number

Closes #394

How to Test

  1. pip install pyyaml pytest
  2. python scripts/sync_extensions.py --check - exits 0 (only the pre-existing, non-blocking coverage warning for plugins/issue-duplicate-checker remains).
  3. python -m pytest tests/test_sync_extensions.py -q - 40 passed.
  4. Confirm the files are real (mode 100644, not 120000): git ls-files -s -- .claude-plugin .codex-plugin .plugin
  5. (Optional, network) After merge to main, curl -fsSL https://raw.githubusercontent.com/OpenHands/extensions/main/.claude-plugin/marketplace.json returns the catalog JSON (HTTP 200) instead of 404. (Cannot verify pre-merge from this branch since the raw URL serves main.)

Video/Screenshots

N/A - file-system / raw-URL fix with no UI surface.

Notes

  • The .claude-plugin/marketplace.json, .codex-plugin/marketplace.json, and .plugin/marketplace.json files are now generated copies; edit the canonical marketplaces/openhands-extensions.json and run python scripts/sync_extensions.py (no args) to refresh all three. Do not reintroduce symlinks at these paths.
  • Per-skill/per-plugin .claude-plugin and .codex-plugin vendor symlinks within skills/ and plugins/ are unrelated and unchanged (managed by the existing symlinks sync task).
  • JSON is canonicalized (sorted keys, 2-space indent) for stable, minimal diffs and reliable byte-for-byte equality checks.

This PR was created by an AI agent (OpenHands) on behalf of a user.

raw.githubusercontent.com does not dereference git symlinks through
directory paths, so the double symlink chain
(.claude-plugin -> .plugin -> ../marketplaces/openhands-extensions.json)
made the catalog unreachable at the standard
.claude-plugin/marketplace.json path. Consumers using
github://openhands/extensions (e.g. openhands/plugin-directory) got a 404
or the literal symlink body and saw an empty Plugin Directory.

Replace the root vendor symlinks with real marketplace.json copies that
mirror the canonical marketplaces/openhands-extensions.json byte-for-byte.
Add a root-marketplace sync task to scripts/sync_extensions.py to keep
them in sync (and CI --check enforces it), plus tests.

Closes #394

Co-authored-by: openhands <openhands@all-hands.dev>
Collapse the three root marketplace copies into a single canonical one at
.plugin/marketplace.json (this repo's canonical manifest dir). Drop the
root .claude-plugin/ and .codex-plugin/ mirrors entirely - they duplicated
the catalog with no raw-URL benefit, since raw.githubusercontent.com
cannot dereference symlinks and the real file already lives at .plugin/.

Per-skill/per-plugin .claude-plugin and .codex-plugin vendor symlinks
(within skills/ and plugins/) are unrelated and remain.

Note: openhands/plugin-directory hardcodes
.claude-plugin/marketplace.json as its fetch path, so it must be updated
to fetch .plugin/marketplace.json for this to resolve #394 on its side.

Co-authored-by: openhands <openhands@all-hands.dev>
@jpelletier1 jpelletier1 changed the title fix: make root marketplace catalog real files (not symlinks) fix: publish marketplace catalog as real file at .plugin/marketplace.json Jul 8, 2026
@jpelletier1 jpelletier1 changed the title fix: publish marketplace catalog as real file at .plugin/marketplace.json fix: make root marketplace catalog real files (not symlinks) Jul 8, 2026
@jpelletier1
jpelletier1 marked this pull request as ready for review July 8, 2026 14:25
@jpelletier1

Copy link
Copy Markdown
Contributor Author

@jpshackelford As someone passionate about Plugins, I wanted to see if you had a chance to review this PR. Specifically, the symlinks we use in the OpenHands/extensions repo makes it difficult to render the full plugin marketplace in the Plugin Directory -- this attempts to fix that by creating an actual marketplace.json in the respective folders and keeping them in sync.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: fix A bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Marketplace catalog unreachable via symlinked .claude-plugin path (breaks plugin-directory / github://openhands/extensions)

2 participants