A catalog file validates, then install looks in the parent directory

John Burns

The install error named <parent>/plugins/probe-plugin. The file just added was <market>/catalog.json. The plugin directory was <market>/plugins/probe-plugin, beside the catalog, not beside the parent. claude plugin validate on that file had exited 0. Its warning had already named plugins[0] plugin.json under <market>.

Those two directories are not two prints of one root. Validate read the plugin from the directory that holds the catalog. claude plugin marketplace add exited 0 and stored the parent as installLocation. Install then looked there. The plugin was never in that parent.

Claude Code 2.1.308 and 2.1.318 printed the same exit codes for this file. The 2.1.318 changelog says validate no longer passes a custom-named marketplace file that marketplace add rejects. On these two binaries, add does not reject the file, and validate still exits 0. An upgrade does not move the directory install will search.

The stored root is the file’s grandparent

A file path passed to claude plugin marketplace add is recorded with "source": "file" and an installLocation two directories above the file. The list command prints the same pair. Relative plugin sources are resolved from installLocation, not from the directory that contains the file, and not from the directory validate just read.

That rule is invisible when the file is the conventional one. <market>/.claude-plugin/marketplace.json has <market> as its grandparent. Directory add of <market> stores <market> as well. Both forms installed probe-plugin@probe-market on both binaries. The success message was Successfully installed plugin: probe-plugin@probe-market (scope: local).

Move the same JSON up one level, to <market>/catalog.json, and the grandparent is <parent>. Add still exits 0. claude plugin marketplace list --json then shows:

{
  "name": "probe-market",
  "source": "file",
  "path": "<market>/catalog.json",
  "installLocation": "<parent>"
}

Install exits 1. Standard error is Source path does not exist: <parent>/plugins/probe-plugin. The path is installLocation plus the relative source ./plugins/probe-plugin. It is not a missing plugin. The plugin is one directory down, next to the catalog.

Put the file one level deeper, at <market>/.claude-plugin/extra/catalog.json, and the grandparent is <market>/.claude-plugin. Add exits 0. Install exits 1 and names <market>/.claude-plugin/plugins/probe-plugin. The real plugin is <market>/plugins/probe-plugin. Validate on that deeper file exited 0 and printed Validation passed with no author warning. The author warning is how this fixture shows that validate opened plugin.json. Its absence means that file was not opened. A clean pass, here, is a pass against a directory that does not hold the plugin.

The marketplace reference, read 3 November 2026, says to save the file at .claude-plugin/marketplace.json. It says a file kept somewhere else has to be declared in extraKnownMarketplaces with path set, because claude plugin marketplace add has no option for it. The help text for add says the source may be a URL, a path, or a GitHub repo. On this binary a path that is the JSON file itself is accepted. The recorded source type is file. That is not the documented path field inside a git source, and it is not a directory add. The reference does not say that a file source’s install root is the file’s grandparent. The list output does.

What the two binaries were given

The copy on PATH was Claude Code 2.1.308. npm view @anthropic-ai/claude-code time records that version at 2026-10-22T04:19:48.902Z. A second copy was installed with npm install --prefix <prefix> --no-save @anthropic-ai/claude-code@2.1.318 and was not put on PATH. That binary printed 2.1.318 (Claude Code). In the same session, npm view @anthropic-ai/claude-code version printed 2.1.318. The registry time entry for 2.1.318 is 2026-11-03T21:55:17.247Z. The GitHub release v2.1.318 is stamped 2026-11-04T00:01:03Z. This note does not collapse those two stamps into one publish event. Both binaries were the versions those commands printed. The comparison was run on 3 November 2026, Linux x86_64.

CLAUDE_CONFIG_DIR pointed at a new directory for each case. The normal Claude home was not the directory under test. A snapshot of that home’s file names, mtimes, and sizes matched after the run. No claude auth login was run. No prompt was sent. ANTHROPIC_API_KEY was unset. The commands were claude plugin validate, claude plugin marketplace add, claude plugin marketplace list, and claude plugin install.

Each marketplace was a new directory with one plugin. The marketplace JSON named probe-market, set owner.name to Probe, and listed one plugin, probe-plugin, with "source": "./plugins/probe-plugin". The plugin directory held .claude-plugin/plugin.json and one skill file. Nothing was fetched. The source is a relative path, so a failed install is a path join, not a clone error.

The decisive adds used --scope local. --scope project writes extraKnownMarketplaces into .claude/settings.json in the shell’s current directory, not into the directory that holds the catalog. Repeated project-scoped adds in one shell overwrote that one object. The install paths below are the local-scope registry, plugins/known_marketplaces.json under the disposable config directory.

An earlier note on this site compared claude auto-mode config on 2.1.308 and 2.1.316. The commands here are the plugin marketplace commands. They do not show what a signed-in session will allow a plugin to do.

The first line names the schema

claude plugin validate does not use one schema for every JSON file you pass. The first line says which one it used. On a marketplace schema it opens each relative plugin and can warn about that plugin’s plugin.json. On a plugin schema it reads the file as a plugin manifest. Marketplace fields then become unknown fields, and the plugins array is not a list of plugins.

<market>/.claude-plugin/marketplace.json prints Validating marketplace manifest and warns that plugin.json has no author. Exit 0. The same bytes, saved as <market>/.claude-plugin/other.json beside that file, print Validating plugin manifest and exit 0 with four warnings:

owner: Unknown field 'owner'. Claude Code ignores it at load time.
plugins: Unknown field 'plugins'. Claude Code ignores it at load time.
version: No version specified. Consider adding a version following semver (e.g., "1.0.0")
author: No author information provided. Consider adding author details for plugin attribution

The second warning is the miss. The file’s plugins array is the catalog. Validate ignored it. The name marketplace.team.json in that same .claude-plugin/ directory did the same thing. So did team-catalog.json. The conventional name marketplace.json is the name that selects the marketplace schema when the file’s parent directory is .claude-plugin. A different name in that directory selects the plugin schema, even when the bytes are a marketplace.

Outside .claude-plugin/, the same bytes are a marketplace again. <market>/catalog.json printed Validating marketplace manifest, warned about the author line in the real plugin.json, and exited 0. The deeper file <market>/.claude-plugin/extra/catalog.json also printed Validating marketplace manifest, but it printed Validation passed and did not mention plugin.json. The schema followed the path. It did not follow the name field inside the JSON. Both files say "name": "probe-market".

A directory check is a third result. Point validate at a directory whose .claude-plugin/ holds only team-catalog.json, and it exits 1. The first line is Validating plugin manifest. The error is No manifest found in directory. Expected .claude-plugin/marketplace.json or .claude-plugin/plugin.json. The file is in the directory. The directory scan does not open it. You have to pass the file path to get the exit 0 that ignores plugins.

Add accepts the file the directory add refuses

claude plugin marketplace add on that directory exits 1. The JSON line is failureCode manifest_missing, and the message is Marketplace file not found at <market>/.claude-plugin/marketplace.json. The human line starts Failed to add marketplace. That is the directory form of a custom name: add refuses it, and validate on the directory also refuses it. Both versions do this. It is not a 2.1.318 change, and it is not a case where validate passed.

Pass the file instead of the directory and add exits 0 on both versions. The message is Successfully added marketplace: probe-market (declared in local settings), or other-market when that was the name in the file. The registry entry stores the file path you passed and the grandparent as installLocation.

For <market>/.claude-plugin/other.json the grandparent is <market>, which is the right install root for ./plugins/probe-plugin. claude plugin install probe-plugin@other-market --scope local then exited 0 and printed Successfully installed plugin: probe-plugin@other-market (scope: local). Validate had ignored the plugins array. Install still found the plugin, because the two-directory climb landed on the marketplace root. A successful install does not mean validate checked the catalog. It means the climb was lucky.

For <market>/catalog.json the climb is not lucky. Add’s success line is the same shape. List shows installLocation as <parent>. Install fails with the missing path under that parent. Validate had already proved the plugin exists beside the catalog, by warning about its author field. The add command did not use the directory that warning used.

The create-a-marketplace page says validation and installation catch different problems. The examples there are a reserved marketplace name, a bad remote repo, and a relative source whose directory does not exist. This run is a fourth split, and it is the opposite of a missing plugin directory. The directory exists. Install is not looking at it. The page also says a path that does not exist fails at install while validate passes. That sentence is true of a relative source validate did not resolve. It does not tell you that add will rewrite the root before install runs.

The 2.1.318 sentence did not move these exits

The release note for v2.1.318, and the changelog page dated 4 November 2026, include this line: fixed claude plugin validate passing a custom-named marketplace file that claude plugin marketplace add rejects. Each pair in this run was executed on 2.1.308 and then on 2.1.318.

Directory validate of a custom name failed on both, exit 1. Directory add failed on both, exit 1, manifest_missing. File validate of catalog.json, team-catalog.json, marketplace.team.json, other.json, and the nested catalog.json passed on both, exit 0. File add of each of those passed on both, exit 0. Install of the canonical directory, of .claude-plugin/marketplace.json as a file, and of .claude-plugin/other.json succeeded on both. Install of <market>/catalog.json and of the nested file failed on both, with the same missing path shape.

No paired exit code changed. The first lines did not change. installLocation did not change. If the note describes a file this run did not construct, this run does not show that file. What it shows is that a custom name is not one behavior. A directory argument and a file argument disagree, and a file inside .claude-plugin/ is checked as a plugin unless its name is marketplace.json. Upgrading the binary on PATH from 2.1.308 to 2.1.318 does not make validate reject the file add just accepted.

Do not read the success line Successfully added marketplace as a statement that install will use the directory you passed. The line names the marketplace name from the JSON and the scope. It does not name installLocation. The list command does.

Verify the list before you install

Use a disposable CLAUDE_CONFIG_DIR. The adds here wrote a registry under that directory. They did not need a login. Keep the normal Claude home out of the test.

Put the marketplace file at .claude-plugin/marketplace.json if you want the directory command and the file command to share a root. Then run both checks against a new config directory:

claude plugin validate <market>
claude plugin marketplace add --scope local --json <market>
claude plugin marketplace list --json
claude plugin install <plugin>@<marketplace-name> --scope local

Exit 0 from validate, with Validating marketplace manifest and a warning that names the plugin you expect, means the relative source was opened from the directory you think is the root. Exit 0 from add is not that check. Read installLocation in the list output and compare it with the directory that contains plugins/. They matched for the conventional file and for a directory add. They did not match for catalog.json in the marketplace root.

If you already have a file that is not named marketplace.json, pass that file to validate and read the first line before you add it. Validating plugin manifest, followed by Unknown field 'plugins', means the catalog was not checked. Adding that file can still succeed, and install can still succeed, when the file’s parent is .claude-plugin and the plugin lives in the grandparent. That success is the climb. It is not evidence that validate read the plugins array.

If the first line says Validating marketplace manifest and the file is not one directory below the marketplace root, do not stop at Validation passed. The nested file produced that sentence and did not open the real plugin.json. After add, list again. If installLocation is not the directory that holds plugins/, install will append ./plugins/<plugin> to the wrong directory and exit 1 with Source path does not exist.

A directory that only contains a custom-named file is the case both commands already reject. Validate exits 1 and asks for marketplace.json or plugin.json. Add exits 1 with manifest_missing. That pair is consistent. The inconsistent pair is the file path: validate can pass, add can pass, and install can look one or two directories away from the plugin the warning just opened.

The reference is still the right page for the conventional path and for a git source whose path you set yourself. It is the wrong page if the question is whether exit 0 on a custom-named file means marketplace add will refuse it, or means install will search the directory that holds the file. On these two binaries, add does not refuse the file, and install searches the grandparent.

Sources

  • Claude Code 2.1.308 on PATH, and Claude Code 2.1.318 installed with npm install --prefix <prefix> --no-save @anthropic-ai/claude-code@2.1.318 and not placed on PATH. Linux x86_64, 3 November 2026. Disposable CLAUDE_CONFIG_DIR for each case. No login and no prompt.
  • npm view @anthropic-ai/claude-code version and npm view @anthropic-ai/claude-code time, same session. Version 2.1.308 is recorded at 2026-10-22T04:19:48.902Z. Version 2.1.318 is recorded at 2026-11-03T21:55:17.247Z. Latest was 2.1.318.
  • GitHub release v2.1.318, stamped 2026-11-04T00:01:03Z, and the Claude Code changelog entry dated 4 November 2026. The custom-named marketplace file sentence is the line this comparison did not see move.
  • Marketplace reference, file location, claude plugin validate arguments, and the statement that add has no option for a file kept outside .claude-plugin/marketplace.json. Read 3 November 2026.
  • Create a marketplace, the split between problems validate reports and problems that appear at add or install. Read 3 November 2026.