From 9e267088b2d66ef55e1aac6a298da55805f5d478 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 26 Mar 2026 09:49:57 +0000 Subject: [PATCH] Fix ENAMETOOLONG plugin install error caused by self-referential source The marketplace.json plugin source used {"source": "github", "repo": "jrenaldi79/harness-engineering"} which points back to the same repo that hosts the marketplace. This caused Claude Code to recursively clone the repo into its own cache directory (harness-engineering/1.0.0/harness-engineering/ 1.0.0/...) until the path exceeded filesystem limits. Fix: change the source to a relative path ("./") since the plugin lives at the marketplace root. Also add self-referential source detection to the marketplace schema test and a cache recursion check to the E2E install test. https://claude.ai/code/session_01JQrEpoK2PjaQ55CTFkHxy6 --- .claude-plugin/marketplace.json | 5 +-- tests/evals/test-marketplace-install.sh | 13 ++++++ tests/scripts/marketplace-schema.test.js | 54 +++++++++++++++++++++--- 3 files changed, 63 insertions(+), 9 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index a4e9729..c3d6bd9 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -8,10 +8,7 @@ { "name": "harness-engineering", "description": "Readiness analysis and enforcement scaffolding for AI coding agent projects.", - "source": { - "source": "github", - "repo": "jrenaldi79/harness-engineering" - }, + "source": "./", "homepage": "https://github.com/jrenaldi79/harness-engineering" } ] diff --git a/tests/evals/test-marketplace-install.sh b/tests/evals/test-marketplace-install.sh index 87eddc8..79801cb 100755 --- a/tests/evals/test-marketplace-install.sh +++ b/tests/evals/test-marketplace-install.sh @@ -149,6 +149,19 @@ fi pass "Plugin installed successfully" +# Check for recursive cache nesting (ENAMETOOLONG bug) +# If the plugin source is self-referential, Claude Code creates deeply nested +# directories like harness-engineering/1.0.0/harness-engineering/1.0.0/... +CACHE_DIR="$HOME/.claude/plugins/cache" +if [ -d "$CACHE_DIR" ]; then + NESTED=$(find "$CACHE_DIR" -maxdepth 6 -type d -name "$PLUGIN_NAME" 2>/dev/null | wc -l) + if [ "$NESTED" -gt 2 ]; then + fail "Recursive cache nesting detected ($NESTED nested '$PLUGIN_NAME' dirs in cache). Self-referential source in marketplace.json?" + else + pass "No recursive cache nesting" + fi +fi + # Verify plugin is listed PLUGIN_LIST=$(cd "$TMP_DIR" && claude plugin list 2>/dev/null) || true echo "$PLUGIN_LIST" > "$RESULT_DIR/plugin-list-output.txt" diff --git a/tests/scripts/marketplace-schema.test.js b/tests/scripts/marketplace-schema.test.js index 363b08e..d787394 100644 --- a/tests/scripts/marketplace-schema.test.js +++ b/tests/scripts/marketplace-schema.test.js @@ -5,6 +5,9 @@ * so that `claude plugin marketplace add` can register the marketplace * and `claude plugin install` can install plugins from it. * + * Also detects self-referential plugin sources that cause recursive + * caching (ENAMETOOLONG errors during plugin install). + * * Can be run standalone: node tests/scripts/marketplace-schema.test.js * Or via Jest if a jest config is present. */ @@ -21,6 +24,26 @@ const MARKETPLACE_PATH = path.resolve( const VALID_SOURCE_TYPES = ['github', 'url', 'git-subdir', 'npm']; +/** + * Derive the marketplace's GitHub repo identifier (owner/repo, lowercase) + * from plugin homepage URLs or the marketplace name. Returns null if the + * repo cannot be determined. + */ +function deriveMarketplaceRepo(manifest) { + // Check plugin homepage URLs for GitHub repo references + if (Array.isArray(manifest.plugins)) { + for (const p of manifest.plugins) { + if (typeof p.homepage === 'string') { + const match = p.homepage.match( + /github\.com\/([^/]+\/[^/]+?)(?:\.git)?(?:\/|$)/ + ); + if (match) return match[1].toLowerCase(); + } + } + } + return null; +} + function validate() { const raw = fs.readFileSync(MARKETPLACE_PATH, 'utf8'); const manifest = JSON.parse(raw); @@ -60,6 +83,11 @@ function validate() { if (!Array.isArray(manifest.plugins) || manifest.plugins.length === 0) { errors.push('plugins must be a non-empty array'); } else { + // Derive the marketplace repo from the homepage or known repo name. + // Used to detect self-referential GitHub sources that cause recursive + // caching and ENAMETOOLONG errors during plugin install. + const marketplaceRepo = deriveMarketplaceRepo(manifest); + for (let i = 0; i < manifest.plugins.length; i++) { const p = manifest.plugins[i]; const prefix = `plugins[${i}]`; @@ -68,21 +96,37 @@ function validate() { errors.push(`${prefix}.name must be a non-empty string`); } - // source must be an object with a valid source type + // source can be a relative path string (e.g. "./plugins/my-plugin") + // or an object with a source type if (typeof p.source === 'string') { - errors.push( - `${prefix}.source must be an object (e.g. {"source": "github", "repo": "owner/repo"}), got string "${p.source}"` - ); + if (!p.source.startsWith('./')) { + errors.push( + `${prefix}.source string must start with "./" (relative path), got "${p.source}"` + ); + } } else if ( typeof p.source !== 'object' || p.source === null || Array.isArray(p.source) ) { - errors.push(`${prefix}.source must be an object`); + errors.push(`${prefix}.source must be a string (relative path) or an object`); } else if (!VALID_SOURCE_TYPES.includes(p.source.source)) { errors.push( `${prefix}.source.source must be one of: ${VALID_SOURCE_TYPES.join(', ')} (got "${p.source.source}")` ); + } else if (p.source.source === 'github' && marketplaceRepo) { + // Detect self-referential GitHub sources: when a plugin's GitHub + // source points to the same repo that hosts the marketplace, Claude + // Code re-clones the repo inside its own cache on every install, + // creating deeply nested directories until ENAMETOOLONG. + const pluginRepo = (p.source.repo || '').toLowerCase(); + if (pluginRepo === marketplaceRepo) { + errors.push( + `${prefix}.source is self-referential: GitHub source "${p.source.repo}" ` + + `points to the same repo as the marketplace. Use a relative path ` + + `(e.g. "source": "./") instead to avoid recursive caching (ENAMETOOLONG).` + ); + } } } }