[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.
| Version |
npm release date (UTC) |
Checking skill path: occurrences in binary |
Behavior |
| 2.1.128 |
2026-05-04 |
3 |
works (user-confirmed) |
| 2.1.131 |
2026-05-06 |
3 |
works (inferred) |
| 2.1.132 |
2026-05-06 |
3 |
works (inferred) |
| 2.1.133 |
2026-05-07 |
2 |
partial refactor |
| 2.1.136 |
2026-05-08 |
0 |
fails |
| 2.1.137 |
2026-05-09 |
0 |
fails |
| 2.1.138 |
2026-05-09 |
0 |
fails |
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 claude-code-darwin-arm64@2.1.128):
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
skills: "./" (string instead of array): same error, same validation.
- Removing the
skills field: falls back to default skills/ subdirectory auto-discovery, which is empty for plugins whose source root contains SKILL.md directly.
- Adding
.claude-plugin/plugin.json with "skills": "./": same error via the plugin.json processing path. Issue #51888 reports this separately.
- 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
Environment
- Claude Code: v2.1.137 (also reproduced on v2.1.138)
- Platform: darwin-arm64
- npm:
@anthropic-ai/claude-code@2.1.137 (binary @anthropic-ai/claude-code-darwin-arm64@2.1.137)
Suggested questions for triage
This report does not assert whether the new behavior is intentional or unintentional. Items that may help triage:
- 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?
- 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."
- 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.
[BUG] v2.1.136 regression: marketplace entry
"skills": ["./"]rejected withPath escapes plugin directoryfor plugins without plugin.jsonSummary
A
marketplace.jsonplugin entry with"skills": ["./"],"strict": false, and asourcedirectory whose root containsSKILL.mddirectly fails to load:plugins-reference#path-behavior-rules documents this pattern as supported:
v2.1.94 introduced it explicitly (CHANGELOG):
Reproduction
Directory layout:
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.mdcontent:Steps (the
@test-marketplacesuffix matches thenamefield ofmarketplace.json):On v2.1.136 / v2.1.137 / v2.1.138:
1 error during load./doctorreports the error quoted in Summary.On v2.1.94 through v2.1.133: the plugin loads and
/test-plugin:test-skillbecomes 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, andPlugin X has no entry.skills defined.Checking skill path:works as a marker because the v2.1.128 binary'sstringsoutput contains it nowhere else.Checking skill path:occurrences in binaryThe closest matching v2.1.136 CHANGELOG entry:
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
nullwhenpath.relative(pluginRoot, path.resolve(pluginRoot, skillPath))returns the empty string:For
_ === "./",path.relativereturns""becauseK === q, so the function returnsnull. Callers treat thenullreturn aspath-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
claude-code-darwin-arm64@2.1.128):This path uses
path.join(which resolves./to the plugin root) and never calls the validation function. The only error type it can emit ispath-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 retainsProcessing N skill paths for plugin X,valid skill paths for plugin, andhas 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
stringsthe way v2.1.128 did. The inference that v2.1.137 now routes this field throughvalidateinstead ofpath.joinis consistent with thepath-traversalerror 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:
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:
The text mentions
.., but the trigger here ispath.relative(pluginRoot, path.resolve(pluginRoot, "./"))returning""(the resolved path equals the plugin root itself), not a..traversal.Workarounds attempted
skills: "./"(string instead of array): same error, same validation.skillsfield: falls back to defaultskills/subdirectory auto-discovery, which is empty for plugins whose source root containsSKILL.mddirectly..claude-plugin/plugin.jsonwith"skills": "./": same error via the plugin.json processing path. Issue #51888 reports this separately.skills/<name>/SKILL.mdsubdirectory: works, but requires movingSKILL.md. Some projects intentionally placeSKILL.mdat the source root, for example projects that distribute the same content as a Claude Desktop ZIP and need a single source-of-truthSKILL.mdlocation.Related issues
plugin.jsondeclaring"skills": "./", which has been rejected since at least v2.1.107. The current report concernsmarketplace.jsonplugin entry withstrict: falsedeclaring"skills": ["./"].source: "./"and ship noplugin.json, each plugin'sskills:filter is ignored and all skills from the sharedskills/directory load under every namespace. Closed by GitHub Action; [BUG] Plugin enable/disable ignored - all skills loaded regardless of settings #13344 is unrelated and concerns plugin enable/disable behavior.plugin.json). Resolved by restructuring 35 plugins.Environment
@anthropic-ai/claude-code@2.1.137(binary@anthropic-ai/claude-code-darwin-arm64@2.1.137)Suggested questions for triage
This report does not assert whether the new behavior is intentional or unintentional. Items that may help triage:
"skills": ["./"]support meant to remain available throughmarketplace.jsonplugin entries (withstrict: falseand noplugin.json)? Or was it always meant to be limited toplugin.json, which has rejected./since v2.1.107?O === ""). That case is "the path is the plugin root," not "the path escapes the plugin root."["./"]example, and a deprecation entry in CHANGELOG would help users find the migration path.