claude-code - 💡(How to fix) Fix [BUG] v2.1.136 regression: marketplace entry `"skills": ["./"]` rejected with `Path escapes plugin directory` for plugins without plugin.json [1 pull requests]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

A marketplace.json plugin entry with "skills": ["./"], "strict": false, and a source directory whose root contains SKILL.md directly fails to load:

<plugin-name> (user)
  skills path escapes plugin directory: ./
  Paths in plugin.json must not use ".." to reference files outside the plugin directory

plugins-reference#path-behavior-rules documents this pattern as supported:

When a skill path points to a directory that contains a SKILL.md directly, for example "skills": ["./"] pointing to the plugin root, the frontmatter name field in SKILL.md determines the skill's invocation name. This gives a stable name regardless of the install directory.

v2.1.94 introduced it explicitly (CHANGELOG):

Plugin skills declared via "skills": ["./"] now use the skill's frontmatter name for the invocation name instead of the directory basename, giving a stable name across install methods

Error Message

On v2.1.136 / v2.1.137 / v2.1.138: 1 error during load. /doctor reports the error quoted in Summary. This is a proxy for code change, not a direct observation of the new error path. The marketplace-entry skills handling in v2.1.94–v2.1.133 contained four distinctive debug log strings: Processing N skill paths for plugin X, Checking skill path: J -> D (exists: M), Found N valid skill paths for plugin X, setting skillsPaths, and Plugin X has no entry.skills defined. Checking skill path: works as a marker because the v2.1.128 binary's strings output contains it nowhere else.

Fixed a skills entry in plugin.json hiding the plugin's default skills/ directory, and listing a file path now shows an error instead of failing silently // path-not-found error pushed (not path-traversal) This path uses path.join (which resolves ./ to the plugin root) and never calls the validation function. The only error type it can emit is path-not-found. I could not extract the v2.1.137 handler code cleanly. The surrounding bytes did not yield readable JS via strings the way v2.1.128 did. The inference that v2.1.137 now routes this field through validate instead of path.join is consistent with the path-traversal error and with the missing v2.1.128-era debug strings, but is not directly proven from the binary disassembly. Maintainers can confirm or refute by inspecting the v2.1.136 diff for the manifest-loading code. The current error message text is:

  1. skills: "./" (string instead of array): same error, same validation.
  2. Adding .claude-plugin/plugin.json with "skills": "./": same error via the plugin.json processing path. Issue #51888 reports this separately.
  • #51888 (OPEN, 2026-04-22, v2.1.117): Same error message, different code path. #51888 reports plugin.json declaring "skills": "./", which has been rejected since at least v2.1.107. The current report concerns marketplace.json plugin entry with strict: false declaring "skills": ["./"].

Root Cause

This is a proxy for code change, not a direct observation of the new error path. The marketplace-entry skills handling in v2.1.94–v2.1.133 contained four distinctive debug log strings: Processing N skill paths for plugin X, Checking skill path: J -> D (exists: M), Found N valid skill paths for plugin X, setting skillsPaths, and Plugin X has no entry.skills defined. Checking skill path: works as a marker because the v2.1.128 binary's strings output contains it nowhere else.

Fix Action

Fix / Workaround

Workarounds attempted

  • #51888 (OPEN, 2026-04-22, v2.1.117): Same error message, different code path. #51888 reports plugin.json declaring "skills": "./", which has been rejected since at least v2.1.107. The current report concerns marketplace.json plugin entry with strict: false declaring "skills": ["./"].
  • #53426 (CLOSED 2026-04-29 as duplicate of #13344, reported on v2.1.119): Different problem on the same field. When multiple plugin entries share source: "./" and ship no plugin.json, each plugin's skills: filter is ignored and all skills from the shared skills/ directory load under every namespace. Closed by GitHub Action; #13344 is unrelated and concerns plugin enable/disable behavior.
  • alirezarezvani/claude-skills #539 and PR #587: External marketplace hit by #51888's path (plugin.json). Resolved by restructuring 35 plugins.

Code Example

<plugin-name> (user)
  skills path escapes plugin directory: ./
  Paths in plugin.json must not use ".." to reference files outside the plugin directory

---

test-marketplace/
├── .claude-plugin/
│   └── marketplace.json
└── plugin-dir/
    └── SKILL.md           # frontmatter: name: test-skill, description: ...

---

{
  "name": "test-marketplace",
  "owner": { "name": "Test" },
  "plugins": [
    {
      "name": "test-plugin",
      "source": "./plugin-dir",
      "skills": ["./"],
      "strict": false
    }
  ]
}

---

---
name: test-skill
description: test skill
---
test body

---

/plugin marketplace add /path/to/test-marketplace
/plugin install test-plugin@test-marketplace
/reload-plugins

---

// Same logic in both v2.1.128 and v2.1.137
function validate(H, _) {
  let q = path.resolve(H),
      K = path.resolve(q, _),
      O = path.relative(q, K);
  if (O === "" || O.startsWith("..") || path.resolve(O) === O) return null;
  return K;
}

---

if (H.skills) {
  y(`Processing ${Array.isArray(H.skills) ? H.skills.length : 1} skill paths for plugin ${H.name}`);
  let Y = Array.isArray(H.skills) ? H.skills : [H.skills],
      w = await Promise.all(Y.map(async (J) => {
        let D = path.join(O, J);  // direct path.join, no validation
        return { skillPath: J, fullPath: D, exists: await fileExists(D) };
      }));
  let j = [];
  for (let { skillPath: J, fullPath: D, exists: M } of w)
    if (y(`Checking skill path: ${J} -> ${D} (exists: ${M})`), M)
      j.push(D);
    else
      // path-not-found error pushed (not path-traversal)
      ...
  if (y(`Found ${j.length} valid skill paths for plugin ${H.name}, setting skillsPaths`), j.length > 0)
    A.skillsPaths = j;
}
RAW_BUFFERClick to expand / collapse

[BUG] v2.1.136 regression: marketplace entry "skills": ["./"] rejected with Path escapes plugin directory for plugins without plugin.json

Summary

A marketplace.json plugin entry with "skills": ["./"], "strict": false, and a source directory whose root contains SKILL.md directly fails to load:

<plugin-name> (user)
  skills path escapes plugin directory: ./
  Paths in plugin.json must not use ".." to reference files outside the plugin directory

plugins-reference#path-behavior-rules documents this pattern as supported:

When a skill path points to a directory that contains a SKILL.md directly, for example "skills": ["./"] pointing to the plugin root, the frontmatter name field in SKILL.md determines the skill's invocation name. This gives a stable name regardless of the install directory.

v2.1.94 introduced it explicitly (CHANGELOG):

Plugin skills declared via "skills": ["./"] now use the skill's frontmatter name for the invocation name instead of the directory basename, giving a stable name across install methods

Reproduction

Directory layout:

test-marketplace/
├── .claude-plugin/
│   └── marketplace.json
└── plugin-dir/
    └── SKILL.md           # frontmatter: name: test-skill, description: ...

Place at test-marketplace/.claude-plugin/marketplace.json:

{
  "name": "test-marketplace",
  "owner": { "name": "Test" },
  "plugins": [
    {
      "name": "test-plugin",
      "source": "./plugin-dir",
      "skills": ["./"],
      "strict": false
    }
  ]
}

test-marketplace/plugin-dir/SKILL.md content:

---
name: test-skill
description: test skill
---
test body

Steps (the @test-marketplace suffix matches the name field of marketplace.json):

/plugin marketplace add /path/to/test-marketplace
/plugin install test-plugin@test-marketplace
/reload-plugins

On v2.1.136 / v2.1.137 / v2.1.138: 1 error during load. /doctor reports the error quoted in Summary.

On v2.1.94 through v2.1.133: the plugin loads and /test-plugin:test-skill becomes available. End-to-end confirmed on v2.1.128.

Bisect

The Checking skill path: debug string disappears from the platform binary (@anthropic-ai/claude-code-darwin-arm64@<version>) between v2.1.133 and v2.1.136. User-observed plugin-load failures start at v2.1.136. No failures on v2.1.133 or earlier.

This is a proxy for code change, not a direct observation of the new error path. The marketplace-entry skills handling in v2.1.94–v2.1.133 contained four distinctive debug log strings: Processing N skill paths for plugin X, Checking skill path: J -> D (exists: M), Found N valid skill paths for plugin X, setting skillsPaths, and Plugin X has no entry.skills defined. Checking skill path: works as a marker because the v2.1.128 binary's strings output contains it nowhere else.

Versionnpm release date (UTC)Checking skill path: occurrences in binaryBehavior
2.1.1282026-05-043works (user-confirmed)
2.1.1312026-05-063works (inferred)
2.1.1322026-05-063works (inferred)
2.1.1332026-05-072partial refactor
2.1.1362026-05-080fails
2.1.1372026-05-090fails
2.1.1382026-05-090fails

The closest matching v2.1.136 CHANGELOG entry:

Fixed a skills entry in plugin.json hiding the plugin's default skills/ directory, and listing a file path now shows an error instead of failing silently

Observations from binary inspection

Comparing the v2.1.128 and v2.1.137 darwin-arm64 binaries:

(1) The path validation function is unchanged across versions. Both binaries contain a function that returns null when path.relative(pluginRoot, path.resolve(pluginRoot, skillPath)) returns the empty string:

// Same logic in both v2.1.128 and v2.1.137
function validate(H, _) {
  let q = path.resolve(H),
      K = path.resolve(q, _),
      O = path.relative(q, K);
  if (O === "" || O.startsWith("..") || path.resolve(O) === O) return null;
  return K;
}

For _ === "./", path.relative returns "" because K === q, so the function returns null. Callers treat the null return as path-traversal, which renders as "<component> path escapes plugin directory: <path>".

(2) The marketplace-entry skills handling in v2.1.128 bypassed this function. The v2.1.128 binary contains this code (offset 0xBFD3258 / 201284952 in [email protected]):

if (H.skills) {
  y(`Processing ${Array.isArray(H.skills) ? H.skills.length : 1} skill paths for plugin ${H.name}`);
  let Y = Array.isArray(H.skills) ? H.skills : [H.skills],
      w = await Promise.all(Y.map(async (J) => {
        let D = path.join(O, J);  // direct path.join, no validation
        return { skillPath: J, fullPath: D, exists: await fileExists(D) };
      }));
  let j = [];
  for (let { skillPath: J, fullPath: D, exists: M } of w)
    if (y(`Checking skill path: ${J} -> ${D} (exists: ${M})`), M)
      j.push(D);
    else
      // path-not-found error pushed (not path-traversal)
      ...
  if (y(`Found ${j.length} valid skill paths for plugin ${H.name}, setting skillsPaths`), j.length > 0)
    A.skillsPaths = j;
}

This path uses path.join (which resolves ./ to the plugin root) and never calls the validation function. The only error type it can emit is path-not-found.

The v2.1.137 binary no longer contains the Checking skill path: debug log or the surrounding distinctive strings. The containing function still retains Processing N skill paths for plugin X, valid skill paths for plugin, and has no entry.skills defined, so the marketplace-entry skills field is still being processed, but through a different code path.

I could not extract the v2.1.137 handler code cleanly. The surrounding bytes did not yield readable JS via strings the way v2.1.128 did. The inference that v2.1.137 now routes this field through validate instead of path.join is consistent with the path-traversal error and with the missing v2.1.128-era debug strings, but is not directly proven from the binary disassembly. Maintainers can confirm or refute by inspecting the v2.1.136 diff for the manifest-loading code.

Documentation status

plugins-reference#path-behavior-rules currently states:

When a skill path points to a directory that contains a SKILL.md directly, for example "skills": ["./"] pointing to the plugin root, the frontmatter name field in SKILL.md determines the skill's invocation name.

The docs have not been updated for the new behavior. No deprecation note or removal-of-support announcement appears in CHANGELOG entries between v2.1.94 and v2.1.138.

The current error message text is:

Paths in plugin.json must not use ".." to reference files outside the plugin directory

The text mentions .., but the trigger here is path.relative(pluginRoot, path.resolve(pluginRoot, "./")) returning "" (the resolved path equals the plugin root itself), not a .. traversal.

Workarounds attempted

  1. skills: "./" (string instead of array): same error, same validation.
  2. Removing the skills field: falls back to default skills/ subdirectory auto-discovery, which is empty for plugins whose source root contains SKILL.md directly.
  3. Adding .claude-plugin/plugin.json with "skills": "./": same error via the plugin.json processing path. Issue #51888 reports this separately.
  4. Restructuring the plugin to use skills/<name>/SKILL.md subdirectory: works, but requires moving SKILL.md. Some projects intentionally place SKILL.md at the source root, for example projects that distribute the same content as a Claude Desktop ZIP and need a single source-of-truth SKILL.md location.

Related issues

  • #51888 (OPEN, 2026-04-22, v2.1.117): Same error message, different code path. #51888 reports plugin.json declaring "skills": "./", which has been rejected since at least v2.1.107. The current report concerns marketplace.json plugin entry with strict: false declaring "skills": ["./"].
  • #53426 (CLOSED 2026-04-29 as duplicate of #13344, reported on v2.1.119): Different problem on the same field. When multiple plugin entries share source: "./" and ship no plugin.json, each plugin's skills: filter is ignored and all skills from the shared skills/ directory load under every namespace. Closed by GitHub Action; #13344 is unrelated and concerns plugin enable/disable behavior.
  • alirezarezvani/claude-skills #539 and PR #587: External marketplace hit by #51888's path (plugin.json). Resolved by restructuring 35 plugins.

Environment

Suggested questions for triage

This report does not assert whether the new behavior is intentional or unintentional. Items that may help triage:

  1. Is the v2.1.94-introduced "skills": ["./"] support meant to remain available through marketplace.json plugin entries (with strict: false and no plugin.json)? Or was it always meant to be limited to plugin.json, which has rejected ./ since v2.1.107?
  2. If still supported, the path validation needs an exemption for the case where the resolved path equals the plugin root itself (O === ""). That case is "the path is the plugin root," not "the path escapes the plugin root."
  3. If no longer supported, plugins-reference#path-behavior-rules needs updating to remove the ["./"] example, and a deprecation entry in CHANGELOG would help users find the migration path.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

claude-code - 💡(How to fix) Fix [BUG] v2.1.136 regression: marketplace entry `"skills": ["./"]` rejected with `Path escapes plugin directory` for plugins without plugin.json [1 pull requests]