A Claude Code plugin marketplace is a folder holding .claude-plugin/marketplace.json, which needs name, owner and plugins, and each plugin it lists is a folder holding its own .claude-plugin/plugin.json, where name is the only required key. Four commands, run from the marketplace folder, take that to an installed plugin: claude plugin validate ., then claude plugin marketplace add ./, then claude plugin install hello@my-marketplace, then claude plugin details hello to see what it loaded. These are the two files as they stood once claude plugin validate . --strict passed:

{
  "name": "my-marketplace",
  "description": "Plugins I use on every machine",
  "owner": { "name": "Your Name" },
  "plugins": [
    { "name": "hello", "source": "./plugins/hello" }
  ]
}
{
  "name": "hello",
  "version": "1.0.0",
  "description": "One skill that says hello",
  "author": { "name": "Your Name" }
}

Every command below ran on this machine with claude --version printing 2.1.299 (Claude Code), in a scratch HOME with its own CLAUDE_CONFIG_DIR (set to ~/.claude inside that HOME) and no network, and the output under each block is what it printed. The scratch path is shortened to ~; nothing else is edited. Where a sentence says what the docs say, it is quoted from the plugin pages on code.claude.com, read on 2026-10-13. The version, as this run recorded it:

claude --version; echo "exit $?"
2.1.299 (Claude Code)
exit 0

The two files, according to the docs

The marketplace guide defines the outer one: “A plugin marketplace is a directory or repository with a .claude-plugin/marketplace.json file that lists your plugins and where to fetch each one.” The marketplace reference gives its required keys, “name, owner, and plugins are required”, and says of each entry in plugins that “name and source are required.” A source that is a folder inside the marketplace is a string starting with ./, and “The marketplace root is the directory that contains .claude-plugin/”, so ./plugins/hello means my-marketplace/plugins/hello, not a path inside .claude-plugin/.

The inner one is the plugin’s manifest. The manifest reference says “name is the only required key.” and “Save the manifest at .claude-plugin/plugin.json under the plugin root.” Everything else in the plugin sits next to .claude-plugin/, not in it; the create page is blunt about it: “Only plugin.json goes inside .claude-plugin/. Components saved there don’t load.” Here the plugin carries one skill, skills/greet/SKILL.md; a plugin skill is the same SKILL.md file with the same frontmatter as a standalone one, which the skills, slash commands and subagents post takes apart field by field. The same page says how it is named once installed: “The skill’s command combines the plugin name and the skill’s name”, so this one would be /hello:greet in a session.

Two names must agree. The entry name in marketplace.json is what people type before the @, and the manifest name is “the prefix on the plugin’s skills, and the name claude plugin details takes.” The guide’s advice is short: “Keep the two names the same.” Both are hello here.

Build a marketplace with too little in it, and validate it

Start in an empty folder, ~/my-marketplace, and write the smallest files that look plausible: a marketplace with no owner, and a plugin manifest with only name.

mkdir -p .claude-plugin plugins/hello/.claude-plugin plugins/hello/skills/greet
cat > .claude-plugin/marketplace.json <<'EOF'
{
  "name": "my-marketplace",
  "plugins": [
    { "name": "hello", "source": "./plugins/hello" }
  ]
}
EOF
cat > plugins/hello/.claude-plugin/plugin.json <<'EOF'
{
  "name": "hello"
}
EOF
cat > plugins/hello/skills/greet/SKILL.md <<'EOF'
---
name: greet
description: Say hello and name the current directory
---

Say hello, then print the current working directory.
EOF
find . -type f | sort
./.claude-plugin/marketplace.json
./plugins/hello/.claude-plugin/plugin.json
./plugins/hello/skills/greet/SKILL.md

claude plugin validate takes a manifest file or a folder; given a folder, it validates .claude-plugin/marketplace.json “when it exists”, and otherwise .claude-plugin/plugin.json.

claude plugin validate .; echo "exit $?"
Validating marketplace manifest: ~/my-marketplace/.claude-plugin/marketplace.json

✘ Found 1 error:

  ❯ owner: Invalid input: expected object, received undefined

⚠ Found 3 warnings:

  ❯ plugins[0] plugin.json → version: No version specified. Consider adding a version following semver (e.g., "1.0.0")
  ❯ plugins[0] plugin.json → description: No description provided. Adding a description helps users understand what your plugin does
  ❯ plugins[0] plugin.json → author: No author information provided. Consider adding author details for plugin attribution

✘ Validation failed
exit 1

One error and three warnings, and the field names come straight from the messages. The error is owner, which the reference lists as an object where “name is required; email and url are optional”. The three warnings are prefixed plugins[0] plugin.json →, which the reference explains: a message with that prefix “is about that plugin’s own files”. So the marketplace check also opened the plugin’s manifest and found no version, description or author. The error alone decides the verdict: ✘ Validation failed and exit 1.

Fix the error, then the warnings

Add an owner. The run below also adds a top-level description, because the reference says “claude plugin validate warns when it’s missing”, and that warning would otherwise be the next thing to fix.

cat > .claude-plugin/marketplace.json <<'EOF'
{
  "name": "my-marketplace",
  "description": "Plugins I use on every machine",
  "owner": { "name": "Your Name" },
  "plugins": [
    { "name": "hello", "source": "./plugins/hello" }
  ]
}
EOF
claude plugin validate .; echo "exit $?"
claude plugin validate . --strict; echo "exit $?"
Validating marketplace manifest: ~/my-marketplace/.claude-plugin/marketplace.json

⚠ Found 3 warnings:

  ❯ plugins[0] plugin.json → version: No version specified. Consider adding a version following semver (e.g., "1.0.0")
  ❯ plugins[0] plugin.json → description: No description provided. Adding a description helps users understand what your plugin does
  ❯ plugins[0] plugin.json → author: No author information provided. Consider adding author details for plugin attribution

✔ Validation passed with warnings
exit 0
Validating marketplace manifest: ~/my-marketplace/.claude-plugin/marketplace.json

⚠ Found 3 warnings:

  ❯ plugins[0] plugin.json → version: No version specified. Consider adding a version following semver (e.g., "1.0.0")
  ❯ plugins[0] plugin.json → description: No description provided. Adding a description helps users understand what your plugin does
  ❯ plugins[0] plugin.json → author: No author information provided. Consider adding author details for plugin attribution

✘ Validation failed (--strict treats warnings as errors)
exit 1

With the error gone, the same three warnings now pass (exit 0), and the same run with --strict fails (exit 1). That is the whole difference between the two; the cli reference describes --strict as “Treat warnings as errors, so unrecognized fields and missing metadata that the runtime tolerates fail the run”. The docs describe the warning case as “the manifest loads, but the validator found something to fix”, so a plugin with these three warnings would still install; --strict is for a pipeline that should not let them through.

Now give the plugin manifest the three fields the warnings named. On the create page, version is “optional. Setting it keeps users on that version until you change it”, and inside author, “name is required inside it; email and url are optional.”

cat > plugins/hello/.claude-plugin/plugin.json <<'EOF'
{
  "name": "hello",
  "version": "1.0.0",
  "description": "One skill that says hello",
  "author": { "name": "Your Name" }
}
EOF
claude plugin validate . --strict; echo "exit $?"
Validating marketplace manifest: ~/my-marketplace/.claude-plugin/marketplace.json

✔ Validation passed
exit 0

Add the marketplace and install the plugin

First, what the config folder held before either command. In this run CLAUDE_CONFIG_DIR is ~/.claude, which is also its default: the env vars page says it overrides “the configuration directory (default: ~/.claude). All settings, session history, and plugins are stored under this path.”

cd ~/.claude && find . -type f | sort
./.claude.json
./backups/.claude.json.backup.1791961002526

Those two files were written by the claude commands already run above, before any marketplace existed: .claude.json and a timestamped copy of it under backups/. Neither is a plugin file. Now register the folder as a marketplace. The argument is a local path, and the cli reference says a path to a directory has the source type directory and Claude Code “Reads the directory in place.”

claude plugin marketplace add ./; echo "exit $?"
Adding marketplace…✔ Successfully added marketplace: my-marketplace (declared in user settings)
exit 0

Before installing, check which flags this version has. claude plugin install --help lists -s, --scope <scope>, with user as the default:

claude plugin install --help; echo "exit $?"
Usage: claude plugin install|i [options] <plugin>

Install a plugin from available marketplaces (use plugin@marketplace for
specific marketplace)

Options:
  --accept-command <sha256>  Accept the marketplace-declared command (a
                             command-source install, or the headersHelper that
                             fetches the archive) whose sha256 a previous --json
                             run reported as shownCommand.sha256; counts as -y
                             for exactly that command, for that plugin and
                             marketplace catalog, and nothing else. If either
                             changed (a refresh that moved the catalog counts),
                             the run refuses and reports the command again, to
                             be shown to a person again
  --config <key=value>       Set a userConfig option declared in the plugin's
                             manifest, or a bundled .mcpb server's own
                             user_config field as <server>.<key>=<value> (a bare
                             key works when only one bundled server declares
                             it). Repeatable. Values are validated against the
                             schema and stored via the same path as the
                             interactive /plugin configure flow.
  -h, --help                 Display help for command
  --json                     Print one machine-readable result line on stdout
                             instead of the human message (same exit codes; a
                             marketplace-declared command is still shown and
                             must be confirmed — pass -y when not interactive)
  --marketplace <source>     Install <plugin> (its bare name) from the
                             marketplace at this source: owner/repo, a git or
                             https URL, or a path. A marketplace not added yet
                             is added first, in user settings, under the same
                             checks as 'plugin marketplace add'
  --registry <url>           For a <package>@npm install: resolve and download
                             from this npm registry instead of the one your npm
                             configuration selects
  -s, --scope <scope>        Installation scope: user, project, or local
                             (default: "user")
  -y, --yes                  Accept the displayed marketplace-declared command
                             without the confirmation prompt — a plugin
                             installed by running a command, or one whose
                             archive is fetched through a headersHelper command
                             (required when stdin or stdout is not a TTY)
exit 0

So --scope user below only spells out the default. The overview page says what each scope means: “User scope: enabled for you in every project on this computer”, while project scope goes “through the committed .claude/settings.json” and local scope is “enabled for you in this repository only”.

claude plugin install hello@my-marketplace --scope user; echo "exit $?"
Installing plugin "hello@my-marketplace"...✔ Successfully installed plugin: hello@my-marketplace (scope: user)
exit 0
claude plugin list; echo "exit $?"
Installed plugins:

  ❯ hello@my-marketplace
    Version: 1.0.0
    Read from: ~/my-marketplace/plugins/hello
    Scope: user
    Status: ✔ enabled

exit 0
claude plugin details hello; echo "exit $?"
hello 1.0.0
  Description: One skill that says hello
  Source: hello@my-marketplace

Component inventory
  Skills (1)  greet
  Agents (0)
  Hooks (0)
  MCP servers (0)
  LSP servers (0)

Projected token cost
  Always-on:   ~13 tok   added to every session

Per-component (rounded)
  component  always-on  on-invoke
  greet           < 20       < 20

  On-invoke cost is paid each time a skill or agent fires.
  Token counts are estimates and may differ from actual usage.
exit 0

claude plugin details reports the plugin’s skill, greet, under Component inventory, and a Projected token cost, which the cli reference describes as “the always-on tokens the plugin adds to every session”. The figures are the tool’s own estimate, and it prints that caveat itself: “Token counts are estimates and may differ from actual usage.”

Every file the install wrote

After marketplace add and install, the same listing:

cd ~/.claude && find . -type f | sort
./.claude.json
./backups/.claude.json.backup.1791961002526
./plugins/cache/my-marketplace/hello/1.0.0/.claude-plugin/plugin.json
./plugins/cache/my-marketplace/hello/1.0.0/skills/greet/SKILL.md
./plugins/installed_plugins.json
./plugins/known_marketplaces.json
./settings.json

Five new files: settings.json, plugins/known_marketplaces.json, plugins/installed_plugins.json, and two files under plugins/cache/my-marketplace/hello/1.0.0. One at a time.

settings.json is the user settings file. marketplace add wrote the extraKnownMarketplaces entry and install wrote the enabledPlugins entry. The loading page describes exactly this: “When you run claude plugin marketplace add, Claude Code writes the marketplace to extraKnownMarketplaces in your user settings as well as to disk”.

cat ~/.claude/settings.json
{
  "extraKnownMarketplaces": {
    "my-marketplace": {
      "source": {
        "source": "directory",
        "path": "~/my-marketplace"
      }
    }
  },
  "enabledPlugins": {
    "hello@my-marketplace": true
  }
}

plugins/known_marketplaces.json is the record of fetched marketplaces, “with its source, installLocation, lastUpdated, and autoUpdate”. For a local folder, installLocation is the folder itself; the loading page says a marketplace added from a local file or directory source “has no copy here, and its installLocation in known_marketplaces.json is the path you gave”. This run wrote no autoUpdate key. lastUpdated is a timestamp, so it is volatile: a second run writes a different value.

cat ~/.claude/plugins/known_marketplaces.json; echo
{
  "my-marketplace": {
    "source": {
      "source": "directory",
      "path": "~/my-marketplace"
    },
    "installLocation": "~/my-marketplace",
    "lastUpdated": "2026-10-14T06:56:43.357Z"
  }
}

plugins/installed_plugins.json “records each install with its scope, installPath, and version”. It also carries installedAt and lastUpdated, both volatile timestamps; the cli reference calls installedAt the “ISO timestamp of the install.” The file has no newline at the end, so the block adds one with echo to keep the output readable.

cat ~/.claude/plugins/installed_plugins.json; echo
{
  "version": 2,
  "plugins": {
    "hello@my-marketplace": [
      {
        "scope": "user",
        "installPath": "~/.claude/plugins/cache/my-marketplace/hello/1.0.0",
        "version": "1.0.0",
        "installedAt": "2026-10-14T06:56:43.766Z",
        "lastUpdated": "2026-10-14T06:56:43.766Z"
      }
    ]
  }
}

The two cache files are a byte-for-byte copy of the plugin folder, at cache/<marketplace>/<plugin>/<version>/ as the loading page lays it out, with <version> here the 1.0.0 that plugin.json sets:

diff -r plugins/hello ~/.claude/plugins/cache/my-marketplace/hello/1.0.0; echo "exit $?"
exit 0

One detail does not line up neatly. installPath points at that cache copy, but claude plugin list printed Read from: ~/my-marketplace/plugins/hello, the source folder. The loading page says a relative-path plugin in a marketplace added from a local path “loads in place from its path inside the marketplace folder”, and that “Your edits to the source directory take effect at the next session start or /reload-plugins, and you don’t need to increase the version.” So on 2.1.299 the install still writes a copy into the cache, and the list says the plugin is read from the source. No session was started here to watch which one a session actually loads.

Disable, enable, uninstall, and remove the marketplace

The four commands, as their own --help describes them on this version:

claude plugin disable --help; echo "exit $?"
claude plugin enable --help; echo "exit $?"
claude plugin uninstall --help; echo "exit $?"
claude plugin marketplace remove --help; echo "exit $?"
Usage: claude plugin disable [options] [plugin]

Disable an enabled plugin

Options:
  -a, --all            Disable all enabled plugins
  -h, --help           Display help for command
  --json               Print one machine-readable result line on stdout instead
                       of the human message (same exit codes)
  -s, --scope <scope>  Installation scope: user, project, local (default:
                       auto-detect)
exit 0
Usage: claude plugin enable [options] <plugin>

Enable a disabled plugin

Options:
  -h, --help           Display help for command
  --json               Print one machine-readable result line on stdout instead
                       of the human message (same exit codes)
  -s, --scope <scope>  Installation scope: user, project, local (default:
                       auto-detect)
exit 0
Usage: claude plugin uninstall|remove [options] <plugin>

Uninstall an installed plugin

Options:
  -h, --help           Display help for command
  --json               Print one machine-readable result line on stdout instead
                       of the human message (same exit codes; not with --prune)
  --keep-data          Preserve the plugin's persistent data directory
                       (~/.claude/plugins/data/{id}/)
  --prune              Also remove auto-installed dependencies that are no
                       longer needed (requires -y in non-interactive contexts)
  -s, --scope <scope>  Uninstall from scope: user, project, or local (default:
                       "user")
  -y, --yes            Skip the --prune confirmation prompt (required when stdin
                       or stdout is not a TTY)
exit 0
Usage: claude plugin marketplace remove|rm [options] <name>

Remove a configured marketplace

Options:
  -h, --help       Display help for command
  --json           Print one machine-readable result line as the last line on
                   stdout (same exit codes)
  --scope <scope>  Remove the marketplace declaration from a specific settings
                   scope: user, project, or local. Omit to remove it from every
                   scope.
exit 0

The cli reference adds that “remove and rm are aliases for uninstall”, and for the marketplace command that “rm is an alias for remove”. Disable flips the enabledPlugins value to false and keeps everything else; disabling twice fails:

claude plugin disable hello@my-marketplace; echo "exit $?"
grep -A2 '"enabledPlugins"' ~/.claude/settings.json
claude plugin disable hello@my-marketplace; echo "exit $?"
✔ Successfully disabled plugin: hello (scope: user)
exit 0
  "enabledPlugins": {
    "hello@my-marketplace": false
  },
exit 1

On stderr:

✘ Failed to disable plugin "hello@my-marketplace": Plugin "hello@my-marketplace" is already disabled

The second disable exits 1 and prints its message on stderr. The cli reference says the same: disabling a plugin that is already disabled “exits 1, as plugin enable does for an already-enabled plugin.” With --json, the failure carries "failureCode": "already_in_goal_state", so a script can tell it apart from a real failure. Enable turns it back to true:

claude plugin enable hello@my-marketplace; echo "exit $?"
grep -A2 '"enabledPlugins"' ~/.claude/settings.json
✔ Successfully enabled plugin: hello (scope: user)
exit 0
  "enabledPlugins": {
    "hello@my-marketplace": true
  },

Uninstall removes the record from installed_plugins.json and the key from enabledPlugins, and leaves the marketplace declared:

claude plugin uninstall hello@my-marketplace; echo "exit $?"
cd ~/.claude && find . -type f | sort
cat plugins/installed_plugins.json; echo
cat settings.json
✔ Successfully uninstalled plugin: hello (scope: user)
exit 0
./.claude.json
./backups/.claude.json.backup.1791961002526
./plugins/cache/my-marketplace/hello/1.0.0/.claude-plugin/plugin.json
./plugins/cache/my-marketplace/hello/1.0.0/.orphaned_at
./plugins/cache/my-marketplace/hello/1.0.0/skills/greet/SKILL.md
./plugins/installed_plugins.json
./plugins/known_marketplaces.json
./settings.json
{
  "version": 2,
  "plugins": {}
}
{
  "enabledPlugins": {},
  "extraKnownMarketplaces": {
    "my-marketplace": {
      "source": {
        "source": "directory",
        "path": "~/my-marketplace"
      }
    }
  }
}

The cache copy is still there, now with an .orphaned_at file in it. The loading page explains it: “When you update or uninstall a plugin, Claude Code writes an .orphaned_at marker into the previous version directory. It removes that directory in a background cleanup 14 days later”, but “The sweep runs only while installed_plugins.json records at least one install.” In this run its content was a number in epoch milliseconds, another volatile value. No plugins/data/ folder appears in any listing above, because this plugin never wrote one, so --keep-data had nothing to keep.

Last, remove the marketplace. The <name> it takes “is the marketplace name that plugin marketplace list shows, not the source you passed to add”:

claude plugin marketplace remove my-marketplace; echo "exit $?"
cd ~/.claude && find . -type f | sort
cat plugins/known_marketplaces.json; echo
cat settings.json
✔ Successfully removed marketplace: my-marketplace
exit 0
./.claude.json
./backups/.claude.json.backup.1791961002526
./plugins/cache/my-marketplace/hello/1.0.0/.claude-plugin/plugin.json
./plugins/cache/my-marketplace/hello/1.0.0/.orphaned_at
./plugins/cache/my-marketplace/hello/1.0.0/skills/greet/SKILL.md
./plugins/installed_plugins.json
./plugins/known_marketplaces.json
./settings.json
{}
{
  "enabledPlugins": {},
  "extraKnownMarketplaces": {}
}

After marketplace remove, known_marketplaces.json is {} and both maps in settings.json are empty, but the files themselves stay, and so does the orphaned cache copy. The docs warn that removing a marketplace from its last scope “also deletes its cache and uninstalls every plugin you installed from it”; nothing was installed any more, and a local directory marketplace has no copy of its own to delete. The folder you built, ~/my-marketplace, is never touched by any of these commands.

Sharing the marketplace from a git repository

Nothing in the two files changes when the folder becomes a repository. The guide’s next step is “push the marketplace directory to a git host”, and then “Your teammates then run claude plugin marketplace add <owner>/<repo> in their shell for a GitHub repository, or the same command with the repository URL.” The hosting page adds that owner/repo “always means github.com”, that a ref is pinned by appending #<ref>, and that the name people install from is your file’s, not the repository’s: “Claude Code takes that name from the name field in your marketplace.json, not from the repository name.”

claude plugin marketplace add your-org/your-marketplace

Not run here: adding a marketplace from GitHub clones it over the network, and this page ran with no network. The line above is the docs’ own example form, not a repository that exists.

Two notes from the same pages matter once it is shared. A relative-path source such as ./plugins/hello needs the whole tree: when users add a bare marketplace.json URL, “Claude Code downloads only that file”, and the hosting page’s advice is to “host the marketplace in a git repository so Claude Code clones the whole tree.” And --scope project on marketplace add declares it in the project’s settings instead of yours, for “Add a marketplace and share it with the project”.

In CI: validate --strict, at the root and per plugin

The cli reference puts it in one line: claude plugin validate ./my-plugin --strict, “Validate a plugin before committing it”. A marketplace needs one more thing. A run from the marketplace root checks each plugin’s manifest, as the first run above showed, but “from a marketplace directory, Claude Code doesn’t open the skill, agent, command, or hook files of plugins the marketplace lists in other directories”. To see that, add a second skill with no frontmatter at all and validate both ways:

mkdir -p plugins/hello/skills/bye
printf 'Say goodbye.\n' > plugins/hello/skills/bye/SKILL.md
claude plugin validate . --strict; echo "exit $?"
claude plugin validate plugins/hello --strict; echo "exit $?"
Validating marketplace manifest: ~/my-marketplace/.claude-plugin/marketplace.json

✔ Validation passed
exit 0
Validating plugin manifest: ~/my-marketplace/plugins/hello/.claude-plugin/plugin.json

Validating skill: ~/my-marketplace/plugins/hello/skills/bye/SKILL.md

⚠ Found 1 warning:

  ❯ frontmatter: No frontmatter block found. Add YAML frontmatter between --- delimiters at the top of the file to set description and other metadata.

✘ Validation failed (--strict treats warnings as errors)
exit 1

The root run passes with exit 0; the per-plugin run reads the new SKILL.md, warns, and fails under --strict with exit 1. A CI job for a marketplace therefore runs claude plugin validate . --strict and then the same command once per plugin folder, and fails on the first non-zero exit. The documented codes are 0 for a pass, 1 for a failure or a warning under --strict, and 2 when “The validator itself failed, such as on an unreadable path”. No run here exited 2.

What ran here and what did not

Ran: claude --version and every claude plugin command shown above, in order, in a scratch HOME and CLAUDE_CONFIG_DIR, with CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC and DISABLE_TELEMETRY set and no network. Every output is what came out; only the scratch path is shortened to ~. The test that guards this page runs the same sequence again in a fresh scratch folder, compares every output with timestamps masked, and checks that the real Claude Code config file on the machine is unchanged.

Not run: any Claude Code session. The skill was never invoked, so what /hello:greet does is quoted from the docs, not observed. Also not run: adding a marketplace from GitHub or a URL, because that needs the network.

← Back to all posts

Follow new posts by RSS: paste dankdev.com/feed.xml into any feed reader. It is a plain XML file — no signup, no email address, no account. Posts land there the same day they go up, and there is nothing to unsubscribe from later.

Back to the top