Summary
When the plugin-directory is configured with MARKETPLACE_SOURCE=github://openhands/extensions, the marketplace catalog does not render — the Plugin Directory shows an empty catalog.
Root cause: the catalog is exposed through a double symlink, and raw.githubusercontent.com does not dereference symlinks through directory paths.
How plugin-directory fetches the catalog
openhands/plugin-directory builds a hardcoded raw URL from the source config (server/app/services/marketplace_service.py):
CATALOG_PATH = ".claude-plugin/marketplace.json"
url = f"https://raw.githubusercontent.com/{owner}/{repo}/{ref}/{catalog_path}"
So it requests exactly:
https://raw.githubusercontent.com/openhands/extensions/main/.claude-plugin/marketplace.json
The symlink chain in this repo
Traced via the GitHub git Trees API (mode=120000 = symlink):
| Path |
Git mode |
Type |
Target |
.claude-plugin |
120000 |
symlink |
.plugin |
.plugin/marketplace.json |
120000 |
symlink |
../marketplaces/openhands-extensions.json |
marketplaces/openhands-extensions.json |
100644 |
real file |
(the actual catalog) |
Verification
raw.githubusercontent.com serves the raw blob content stored in git. A symlink is stored as a blob whose content is the link-target text, and the raw server does not traverse symlinked directory paths:
.claude-plugin/marketplace.json → HTTP 404 (symlinked directory not traversable)
.plugin/marketplace.json → returns the literal string ../marketplaces/openhands-extensions.json (the symlink blob body), not the JSON catalog — so even if level 1 resolved, JSON parsing would fail
marketplaces/openhands-extensions.json → HTTP 200, the real catalog JSON
As a result, plugin-directory's fetch_catalog() raises CatalogUnavailableError on the 404, the plugins list comes back empty, and nothing renders. This is a server-side (GitHub raw) limitation, not something the plugin-directory client can work around — the catalog simply never arrives.
Suggested fix
- In this repo (simplest): commit a real
.claude-plugin/marketplace.json file (or a real .claude-plugin/ directory) instead of the symlink chain, so the catalog is reachable at the path plugin-directory (and other consumers using the standard .claude-plugin/marketplace.json convention) expects.
Happy to open a PR for either direction if helpful.
This issue was created by an AI agent (OpenHands) on behalf of a user.
Summary
When the plugin-directory is configured with
MARKETPLACE_SOURCE=github://openhands/extensions, the marketplace catalog does not render — the Plugin Directory shows an empty catalog.Root cause: the catalog is exposed through a double symlink, and
raw.githubusercontent.comdoes not dereference symlinks through directory paths.How plugin-directory fetches the catalog
openhands/plugin-directorybuilds a hardcoded raw URL from the source config (server/app/services/marketplace_service.py):So it requests exactly:
The symlink chain in this repo
Traced via the GitHub git Trees API (
mode=120000= symlink):.claude-plugin120000.plugin.plugin/marketplace.json120000../marketplaces/openhands-extensions.jsonmarketplaces/openhands-extensions.json100644Verification
raw.githubusercontent.comserves the raw blob content stored in git. A symlink is stored as a blob whose content is the link-target text, and the raw server does not traverse symlinked directory paths:.claude-plugin/marketplace.json→ HTTP 404 (symlinked directory not traversable).plugin/marketplace.json→ returns the literal string../marketplaces/openhands-extensions.json(the symlink blob body), not the JSON catalog — so even if level 1 resolved, JSON parsing would failmarketplaces/openhands-extensions.json→ HTTP 200, the real catalog JSONAs a result, plugin-directory's
fetch_catalog()raisesCatalogUnavailableErroron the 404, the plugins list comes back empty, and nothing renders. This is a server-side (GitHub raw) limitation, not something the plugin-directory client can work around — the catalog simply never arrives.Suggested fix
.claude-plugin/marketplace.jsonfile (or a real.claude-plugin/directory) instead of the symlink chain, so the catalog is reachable at the path plugin-directory (and other consumers using the standard.claude-plugin/marketplace.jsonconvention) expects.Happy to open a PR for either direction if helpful.
This issue was created by an AI agent (OpenHands) on behalf of a user.