Repository navigation
Plugin Loader
This page describes how Claude Code finds a marketplace, installs a plugin, and loads its parts.
Read it if you publish a marketplace or a plugin, or if you debug a plugin load.
The facts are for Claude Code 2.1.296.
- Official docs, read on 2026-10-13: the plugin manifest reference, the plugin loading reference, the marketplace guide, and the mods reference. Where a fact comes from these docs, the text says so.
- Binary: I searched the extracted strings of
2.1.296withgrep. The strings name the variables, the file names, and the messages that this page cites. I copied no bundle code. - Repository:
.claude-plugin/marketplace.json,plugins/*/.claude-plugin/plugin.json, and the tests intests/support/.
A marketplace is a folder or a repository with .claude-plugin/marketplace.json.
The file has a name, an owner, and a plugins array.
Each plugin entry has a name and a source.
The repository marketplace is dotclaude.
Its entries list the core plugin and five add-on plugins.
claude plugin marketplace add registers a marketplace.
The docs show a GitHub owner/repo and a local path as arguments.
The command writes the marketplace to extraKnownMarketplaces in user settings and to disk.
Claude Code records each fetched marketplace in known_marketplaces.json.
The binary contains this file name.
The plugins root is ~/.claude/plugins.
The variable CLAUDE_CODE_PLUGIN_CACHE_DIR moves it.
The binary contains that variable name.
The install id is the entry name, an @, and the marketplace name, as in dotclaude@dotclaude.
An install runs these steps, according to the docs.
- For a
name@marketplaceinstall, Claude Code refreshes that marketplace first. The refresh does not run when the marketplace is a local path. It also does not run whenCLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFICis set. - Claude Code copies the plugin to
cache/<marketplace>/<plugin>/<version>/. A relative-path plugin in a marketplace that you added from a local path loads in place, with no copy. - Claude Code records the install in
installed_plugins.json. - Claude Code writes
"<name>@<marketplace>": trueunderenabledPluginsin the settings file of the scope.
The scopes are user, project, and local.
User scope enables the plugin in every project.
Project scope writes to the committed .claude/settings.json.
Local scope writes to .claude/settings.local.json.
At session start, Claude Code reads installed_plugins.json and the cache, with no network access.
After the start, it checks the declared marketplaces in the background.
A running session does not see a change until /reload-plugins runs or a new session starts.
When two enabled plugins share a manifest name, one copy wins.
A --plugin-dir plugin replaces an installed plugin of the same name.
The manifest is .claude-plugin/plugin.json.
It is optional, but name is the only required key.
Component folders go at the plugin root, not inside .claude-plugin/.
| Key | What it loads or does | Use in dotclaude |
|---|---|---|
name |
The namespace of every component | dotclaude |
version |
Pins users to the cached copy until the value changes | 0.28.0 |
userConfig |
Values that Claude Code prompts for when the plugin is enabled | mainHandover |
skills |
Adds directories to the default skills/ scan |
Not set |
commands |
Replaces the default commands/ scan |
Not set |
agents |
Replaces the default agents/ scan |
Not set |
hooks |
Adds hook files or inline hooks to hooks/hooks.json
|
Not set |
mcpServers |
Adds MCP servers to .mcp.json
|
Not set |
lspServers |
Adds language servers to .lsp.json
|
Not set |
outputStyles |
Replaces the default output-styles/ scan |
Not set |
settings |
Applies only agent and subagentStatusLine
|
Not set |
homepage, repository, keywords, description
|
Listing and discovery metadata | Set |
The hooks key takes a file that wraps its event map in a top-level hooks key.
The docs say that a hooks file without that wrapper fails to load.
Each component has a default location under the plugin root. A default location loads when the manifest does not replace it.
| Location | Loads |
|---|---|
skills/<name>/SKILL.md |
Skills |
commands/ |
Flat command files |
agents/ |
Subagent files |
hooks/hooks.json |
Hooks, or a hooks module |
.mcp.json |
MCP servers |
.lsp.json |
Language servers |
output-styles/ |
Output styles |
monitors/monitors.json |
Background monitors, in interactive sessions only |
settings.json |
Agent and status line defaults |
bin/ |
Executables on the Bash tool PATH while the plugin is enabled |
The docs say that files in bin/ run as bare commands.
They also say that claude.ai and Cowork do not install a plugin with a bin/ folder.
The binary has a pattern that matches a command under ${CLAUDE_PLUGIN_ROOT}/bin/.
That pattern supports the docs.
It does not prove the PATH behavior alone.
dotclaude uses the default locations of skills/, agents/, output-styles/, hooks/, and bin/.
Its bin/ holds six files: dotclaude-decide, dotclaude-handoff, dotclaude-run-checks, dotclaude-setup, dotclaude-spec, and dotclaude-statusline.
A component path must start with ./.
It must stay inside the plugin root, and it must exist.
${CLAUDE_PLUGIN_ROOT} resolves to the absolute path of the installed version folder.
A hook command receives it in its environment, and a shell-form command must quote it.
The docs say that the variable is absent from commands that Claude runs through the Bash tool.
The root changes on each version.
Keep durable state in ${CLAUDE_PLUGIN_DATA}, which the docs place at data/<plugin-id>/.
The binary contains both CLAUDE_PLUGIN_ROOT and CLAUDE_PLUGIN_DATA.
The repository test tests/support/plugin-hooks-exist.test.ts reads command hooks that use ${CLAUDE_PLUGIN_ROOT}/<file>.
It checks that each file exists.
The paths are relative to ~/.claude/plugins/.
CLAUDE_CODE_PLUGIN_CACHE_DIR moves the root.
| Path | Holds |
|---|---|
cache/<marketplace>/<plugin>/<version>/ |
One copy for each installed version |
data/<plugin-id>/ |
The ${CLAUDE_PLUGIN_DATA} folder, which survives updates |
marketplaces/<name>/ |
A clone of a marketplace from GitHub, another git host, or a URL |
installed_plugins.json |
One record for each install, with its scope, path, and version |
known_marketplaces.json |
One record for each fetched marketplace |
A relative-path plugin in a local marketplace has no copy.
Its ${CLAUDE_PLUGIN_ROOT} points at the source folder.
When a plugin updates, the old version folder receives an .orphaned_at marker.
The sweep removes that folder 14 days later.
The sweep runs only while installed_plugins.json records at least one install.
The binary contains the marker name.
The version decides an update.
The manifest version comes first.
Then the entry version decides, and then the commit SHA or file digest of the source.
dotclaude sets version in its manifest.
So a user keeps the cached copy until that string changes.
That is inference from the docs, not a test.
The dotclaude hooks/hooks.json has no hooks key.
It has one key, modules, with one path: "modules": ["./register.ts"].
The official mods reference defines this form.
A hooks module is a file that exports register(on, options).
Each on(event, handler) call registers a handler for one event.
options holds the userConfig values with defaults filled in.
dotclaude generates plugins/dotclaude/hooks/register.ts from the slices in src/.
just generate writes it, so do not edit it by hand.
The events, and the eleven dotclaude events, are in Hook Events.
The path from src/ to plugins/ is in Architecture.
The modules form and the hooks form can appear in one file.
The codegraph plugin uses both forms.
The core plugin uses only modules.
claude plugin validate <dir> checks a plugin or a marketplace.
It prints Validation passed, Validation passed with warnings, or Validation failed.
--strict turns each warning into a failure.
--json prints a report for a script.
For a plugin with a hooks module, the command also lists the events and the API calls that the module makes.
The binary contains claude plugin validate, Validation passed, and the message that names both manifest files.
claude plugin test <dir> runs each file that ends in .test.ts under the folder.
It exits with status 1 when a test fails.
tools/lab.ts runs it for each plugin folder that has a tests/ folder.
just lab runs that tool.
The repository tests cover the manifests.
tests/support/manifests.test.ts checks that the marketplace lists each plugin once.
It checks that each plugin.json description equals its marketplace entry.
It checks the $schema, the keywords, and the homepage.
tests/support/plugin-hooks-exist.test.ts reads only the hooks key of hooks/hooks.json.
For dotclaude, that key is absent, so the test checks no command.
It does not check that register.ts exists.
- The cache folder layout comes from the docs. I did not find the folder string in the binary.
- I did not run
claude plugin validateorclaude plugin teston this repository. The main session runs those checks.
- Start
- How dotclaude Works
- Add-On Plugins
- How Claude Code Works
- Reference
- Rationale
- Evidence
- Development
- Archive