How plugins load
Trace where each plugin comes from, which settings scope switches it on, what sits on disk under ~/.claude/plugins, and why an update did nothing.
This is the page to open when a plugin did not load, a different copy loaded than the one you expected, or an update apparently changed nothing. It sets out the rules Claude Code follows at session start and on every /reload-plugins. I have found it is also a useful page to hand to Claude itself: point it here and ask it to work out why your setup is misbehaving.
For the normal install, enable and update steps, see Install plugins. If you have a specific error message, Plugin troubleshooting is organised by message.
The three stages
Every plugin you can use has passed through three stages. When something is off, work out which stage it stalled at.
| Stage | Where it lives | What it contains |
|---|---|---|
| Declared | Settings files | enabledPlugins says which plugins should be on; extraKnownMarketplaces says which marketplaces should exist. claude plugin marketplace add writes to your user settings as well as to disk |
| Fetched | ~/.claude/plugins/ | known_marketplaces.json (each fetched marketplace with source, installLocation, lastUpdated, autoUpdate; one per user, so a marketplace added in one project is available in all), installed_plugins.json (each install with scope, installPath, version), and cache/ holding the files |
| Loaded | The running session | Whatever was loaded at start-up or the last /reload-plugins |
Changes to settings or files never reach the loaded stage by themselves. That is why claude plugin update finishes with Restart to apply changes. and background updates tell you to Run /reload-plugins to apply.
Things missing from disk at start-up
Start-up loads plugins from installed_plugins.json and the cache without touching the network. Once the session is running, Claude Code checks declared marketplaces in the background:
- A marketplace declared in settings but absent from
known_marketplaces.jsongets cloned, then plugins are reloaded and any enabled plugins not yet cached are downloaded. - A marketplace whose source changed in settings gets re-fetched from the new source, and you see
Plugins changed. Run /reload-plugins to activate.
An enabled plugin that neither route fetched, and that has no usable cache directory, shows Plugin "<name>" not cached at <path> in the /plugin Errors tab, and claude plugin list adds a hint to run /plugin to refresh.
Identifying where a plugin came from
Plugin ids take the form <name>@<origin>. You see them in settings and in claude plugin list --json. The origin tells you how the plugin arrived:
| Origin suffix | How it got there | How it is switched on and off |
|---|---|---|
@<marketplace> | Installed from a marketplace you added | "<name>@<marketplace>": true or false in enabledPlugins |
@inline | --plugin-dir, --plugin-url, the CLAUDE_CODE_PLUGIN_DIRS variable, or the Agent SDK plugins option. Lasts for one session | On, unless the manifest has defaultEnabled: false or settings set "<name>@inline": false |
@skills-dir | A folder with .claude-plugin/plugin.json under ~/.claude/skills/ or the project's .claude/skills/ | The manifest's defaultEnabled, unless settings set "<name>@skills-dir" explicitly |
@synced | Turned on for your claude.ai account (by you or your organisation) and downloaded by Claude Code | On, unless the manifest has defaultEnabled: false or settings set "<name>@synced": false. Organisation-required plugins always load |
inline, skills-dir and synced are reserved, so no marketplace may use those names.
Two names for one plugin
For a marketplace plugin, <name> in the id is the entry name from marketplace.json. That is the key in enabledPlugins, the name of the cache folder and what claude plugin list prints. The plugin also has a manifest name in its plugin.json, which namespaces its components and is what name conflicts compare. For @inline and @skills-dir plugins, the id uses the manifest name. Keeping the two names identical saves a lot of confusion.
Plugins shared via a repository
To share a plugin through a repo, either enable it in the committed .claude/settings.json or put it under .claude/skills/. Claude Code does not scan a project's .claude/plugins/ folder.
A few constraints apply:
- Cloud sessions never show the workspace trust dialog, so they do not add marketplaces from a repository's
extraKnownMarketplaces. - A project skills-directory plugin only loads from
.claude/skills/in the session's primary working directory, and only once that folder is trusted. Unlike plain skills it does not search up to the repository root, so launching from a subfolder misses it. Launch from the root, or use/cd(v2.1.246 or later) to move there. - Project-scope plugins come from whoever committed them, not from you, so they need the same trust as project allow rules. Trusting a parent folder or running with
-pis not enough. Code-running parts are further restricted: their MCP servers go through the same per-server approval as a project.mcp.json(see MCP); MCP bundles (.mcpb,.dxt) and server definitions from files outside the plugin are skipped; and background monitors do not load.
Personal-scope plugins have none of these limits.
Plugins synced from claude.ai
Plugins you (or your organisation) enable on claude.ai also load in Claude Code, as <name>@synced, with no marketplace and no install record. In the terminal their skills, agents, hooks, MCP servers and LSP servers all load with the same trust as an installed marketplace plugin.
Syncing happens in two places:
- Cowork sessions download synced plugins into the session's environment at start.
- Terminal sessions sync once in the background each launch, adding new plugins, updating changed ones and removing those switched off. This needs v2.1.273 or later.
Because the terminal sync is in the background it may finish after you have started; you then see Plugins changed. Run /reload-plugins to activate. A plugin enabled on claude.ai mid-session arrives at your next launch.
Terminal sync only happens when you are signed in with your claude.ai account. It does not happen, even after /login, when:
ANTHROPIC_AUTH_TOKEN,CLAUDE_CODE_OAUTH_TOKENor anapiKeyHelpersupplies the credential;- the session does not fetch feature flags (for example with
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFICset; see Environment variables); - the session is in bare mode or started with
--safe-mode; --setting-sourcesomitsuser.
A sign-in from an older version only gains plugin access once it is silently renewed. Running /login again speeds that up, and syncing starts at the next launch.
To control what loads:
- One plugin:
claude plugin disable <name>@synced, or the Installed tab in/plugin, writes"<name>@synced": falseto user settings. Put the same key in the committed project settings to keep it out of a repo everywhere. - All synced plugins: set
syncClaudeAiPluginstofalsein user settings (or your organisation sets it in managed settings). Downloading stops, and at the next launch already-synced plugins move to~/.claude/plugins/.trash/. Turning off Skills for the organisation on claude.ai also stops syncing. - Required plugins: a plugin your organisation marks as required loads regardless.
claude plugin disablerefuses with a message saying it is required by your organisation, andclaude plugin listlabels itrequired by your org.
Finding where a plugin is enabled
enabledPlugins can be set in six sources. From lowest to highest precedence:
| Source | File | Applies to |
|---|---|---|
--add-dir | .claude/settings.json or .claude/settings.local.json in a directory passed with --add-dir | This session only. Only true has any effect, and every other source beats it |
user | ~/.claude/settings.json | You, everywhere |
project | .claude/settings.json | Everyone who clones the repo |
local | .claude/settings.local.json | You, in this repo |
flag | The file passed to --settings | This session only |
managed | Managed settings | Everyone the policy covers. true forces on and false blocks; nothing overrides either |
Merging is per plugin id: the highest-precedence source that mentions an id decides its value, and sources that do not mention it leave lower values in place. More on the files themselves is on Settings.
"I disabled it, but it still loads"
A false in ~/.claude/settings.json loses to a true anywhere higher. The plugin's row in claude plugin list and /plugin says so, for example Disabled in ~/.claude/settings.json but still loads, followed by which source re-enabled it: project, project, gitignored (meaning .claude/settings.local.json), cli flag or managed. To opt out of a project-enabled plugin just for yourself, set it to false in .claude/settings.local.json.
"Enabled in project settings but not installed"
If the only true for a plugin is in a project's .claude/settings.json, Claude Code does not fetch it onto a machine that lacks it, unless its marketplace entry uses a relative path (which loads straight from the marketplace) or a seed directory already holds it. The Errors tab shows Plugin "<name>" is enabled in project settings but isn't installed here.
Plugins with external sources are only fetched when one of these sets them to true: user settings, an untracked .claude/settings.local.json, --settings, or managed settings. The usual fix is claude plugin install <name>@<marketplace> --scope project.
What is on disk
All plugin state lives under one root, ~/.claude/plugins by default or wherever CLAUDE_CODE_PLUGIN_CACHE_DIR points. Paths below are relative to that root.
| Path | Contents |
|---|---|
cache/<marketplace>/<plugin>/<version>/ | One folder per installed version of a marketplace plugin. <plugin> is the entry name and <version> the computed version. ${CLAUDE_PLUGIN_ROOT} points here |
data/<plugin-id>/ | The plugin's persistent folder, ${CLAUDE_PLUGIN_DATA}. Created on first use, kept across updates, and by default deleted when the plugin is uninstalled from its last scope (see --keep-data on the plugin CLI reference) |
marketplaces/<name>/ | Clone or download of a marketplace from GitHub, another git host or a URL. Local file and directory marketplaces have no copy here; their installLocation is the path you supplied |
synced/ | Plugins synced from claude.ai |
.trash/ | Synced plugins that were removed |
installed_plugins.json, known_marketplaces.json | The install and marketplace records. Marketplaces hosted on claude.ai go in known_marketplaces_claudeai.json instead |
flagged-plugins.json | Plugins uninstalled because their marketplace delisted them; shown under Flagged in /plugin |
installed_plugins.set-aside.<date>.<hash>.json, installed_plugins.unreadable.<date>.<hash>.kept | Dated backups made before dropping unusable records or rebuilding an unreadable installed_plugins.json. Removed on the cleanupPeriodDays schedule |
Since ${CLAUDE_PLUGIN_ROOT} is version-specific and changes on every update, anything a plugin needs to keep belongs in ${CLAUDE_PLUGIN_DATA}. Plugin components covers both variables.
In place or copied
| Kind of plugin | How it loads |
|---|---|
--plugin-dir and skills-directory plugins | In place, never copied. A --plugin-url archive or --plugin-dir zip is first unpacked into a session temp folder |
| Relative-path plugins in a marketplace added from a local path | In place from the marketplace folder. Edits apply at next start or /reload-plugins without a version bump, and hooks, MCP and LSP servers get a CLAUDE_PLUGIN_ROOT pointing at the source folder |
command sources in link mode | In place, through links in the cache entry |
| Every other marketplace plugin | Copied into cache/<marketplace>/<plugin>/<version>/. Files outside the plugin folder are not copied, so a script reaching for ../shared will not find it |
Paths that escape the plugin
Regardless of how it loads, a plugin may not declare components outside its own folder. A component path is rejected if it points outside as written (../common), follows a symlink outside (other than links between plugins in the same marketplace), or, on macOS and Linux, contains a backslash anywhere. Always write component paths with forward slashes. Rejections show as path escapes plugin directory (see Errors) and the plugin loads without that component.
Old versions are cleaned up later
On update or uninstall, Claude Code drops an .orphaned_at marker into the old version folder and deletes it in a background sweep 14 days later, so sessions still using it are not pulled out from under. The sweep only runs while at least one install exists; uninstall everything and orphans linger until your next install.
Node.js dependencies
When a plugin is copied into the cache, Claude Code also installs the npm or Bun packages listed in the plugin's own package.json, so hooks and MCP servers can import them. (Dependencies on other plugins are a different mechanism; see Plugin dependencies.)
The install runs whenever a new version folder is created: on install, on update, and at session start for an enabled plugin that is not cached yet. It is not run for in-place relative-path plugins; install those yourself, or from a hook into ${CLAUDE_PLUGIN_DATA}.
It only happens when the plugin root contains both package.json and a supported lockfile. The first lockfile found, in this order, picks the tool:
| Lockfile | Tool |
|---|---|
bun.lock | Bun |
npm-shrinkwrap.json | npm |
package-lock.json | npm |
It is skipped for bun.lockb (binary, cannot be checked), yarn.lock, pnpm-lock.yaml, an npm lockfile with lockfileVersion other than 2 or 3, and a bun.lock with lockfileVersion above 2. The chosen tool must be on the user's PATH; there is no fallback. An npm lockfile reaches the most people. For plugins distributed via an npm source, use npm-shrinkwrap.json, because npm strips package-lock.json from published packages.
The install is locked down so that no plugin code runs during it:
- Every dependency must be a registry package pinned to an exact version. Git, GitHub, folder, workspace or linked dependencies mean no install.
- Download URLs must be
https, unless they point at the user's own default npm registry. - The package manager runs in a separate folder holding only a copy of the dependency list, so it never reads the plugin's
.npmrc,.envorbunfig.toml. On success,node_modulesis moved into the plugin. - Resolution is frozen to the lockfile, and the install is skipped if
package.jsonand the lockfile disagree. --ignore-scriptsis used, so native modules that build inpostinstalldownload but are not compiled.- npm
overrides(with an npm lockfile) or BunpatchedDependencies(withbun.lock) prevent the install. - It times out after 60 seconds.
You cannot turn this behaviour off. On restricted networks, allow the hosts listed in Network configuration.
If the install fails or is skipped, the plugin still loads but parts that need the packages may break. /plugin and claude plugin list annotate an enabled plugin whose cached copy has a lockfile and runtime dependencies but no node_modules, saying whether the install did not finish or cannot run. For anything the automatic install cannot handle (packages that must compile, Python dependencies, Yarn or pnpm projects, git dependencies), install from a hook into the persistent data folder.
Versions and updates
When an author has pushed new commits but claude plugin update replies <name> is already at the latest version (<version>)., the computed version has not changed, so nothing on disk changes. Updates, manual or automatic, recompute the version and leave the cache alone when it matches installed_plugins.json. (A manual update can still retry an unfinished dependency install.) The version also names the cache folder.
Two cases ignore version strings: in-place plugins from a locally added marketplace always load current files, and plugins from a marketplace hosted on claude.ai use the version claude.ai records rather than the manifest's.
How the version is computed
For every source type except command:
versionin the plugin's manifest, if set.- Otherwise
versionon the marketplace entry. - Otherwise, derived from the source:
| Source | Fallback version |
|---|---|
github, url, git-subdir | Source commit SHA, first 12 characters. git-subdir adds a hash of the subdirectory path |
archive | SHA-256 (12 characters) from the entry's sha256 pin, or of the downloaded file if unpinned |
| Relative path in a git-hosted marketplace | Commit SHA of the installed folder |
| Local folder where neither plugin nor marketplace is a git repo | unknown |
npm | unknown |
Claude Code never takes a version from an enclosing repository, such as a ~/.claude you happen to manage with git.
command sources always derive the version from the produced output: a 12-character hash, or <manifest version>-<hash> if the manifest sets a version. The entry's version is ignored for them.
The practical upshot: a manifest pinned to "version": "1.0.0" keeps every user on their cached copy until that string changes. Omit version in both places to have users track commits. Host a marketplace discusses which approach suits which release process.
When an install refreshes the catalogue
Installs look plugins up in the local copy of the marketplace catalogue. Whether that copy is refreshed first depends on how you ask:
| You type | Where | Refreshed |
|---|---|---|
name@marketplace | /plugin install or claude plugin install | That marketplace, before lookup |
name only | /plugin install | Only auto-updating marketplaces, and only after a miss |
name only | claude plugin install | Nothing; cached catalogues are used |
The name@marketplace refresh ignores auto-update settings and DISABLE_AUTOUPDATER. If it fails, the install continues from cache and the CLI reports marketplace not refreshed. It is skipped for local file or directory marketplaces and inline settings-source marketplaces, for marketplaces supplied by a seed directory, if a refresh happened in the last 30 seconds, when CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC is set, and when managed settings block the marketplace (in which case the install is refused too).
When auto-update runs
In an interactive session, after your first message Claude Code waits a random delay of up to ten minutes, then refreshes every auto-updating marketplace and updates its plugins on disk. The current session keeps what it loaded and shows Plugin updated: <name> · Run /reload-plugins to apply. Either way, the new version loads at next launch.
Whether a marketplace auto-updates is decided by the first of these that is set:
autoUpdateon itsextraKnownMarketplacesentry in a settings file.autoUpdateinknown_marketplaces.json, written by Enable auto-update under/plugin> Marketplaces (which also writes to the settings entry if one exists).- The default: on for Anthropic's official marketplaces such as
claude-plugins-official, off forknowledge-work-pluginsandfirst-party-plugins, on for marketplaces added from claude.ai, and off for everything else.
DISABLE_UPDATES=1, DISABLE_AUTOUPDATER=1 or CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 turn the whole pass off and hide the toggle, unless FORCE_AUTOUPDATE_PLUGINS=1 is also set. Plugins whose entry declares a headersHelper are always skipped and appear in the Errors tab for manual update.
After a mid-session update of a copied plugin, hooks, monitors, MCP servers and LSP servers keep using the old path. /reload-plugins switches hooks, MCP and LSP servers over; monitors need a restart.
When a command source re-runs
command-source plugins ignore the auto-update pass. Claude Code re-runs the accepted command:
- on every install or update of the plugin;
- once per session per enabled command-sourced plugin, in the background shortly after start, regardless of auto-update settings or
DISABLE_AUTOUPDATER; - at start-up or
/reload-pluginsif the installed version is missing from the cache.
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC suppresses the two background runs but not explicit installs or updates. If the hashed output changed, the result becomes a new version and is reloaded into the running session (the same components /reload-plugins switches), with a notification. If that would invalidate the prompt cache, you are asked to run /reload-plugins instead, which warns about the cost and applies when re-run with --force; Prompt caching explains why.
Name conflicts
When enabled plugins from different origins share a manifest name, precedence (highest first) is:
- A plugin whose id appears in managed
enabledPlugins(either value). A--plugin-dircopy with a matching manifest name is not loaded, with the message--plugin-dir copy of "<name>" ignored: plugin is locked by managed settings. - A session-only plugin from
--plugin-dir,--plugin-urlorCLAUDE_CODE_PLUGIN_DIRS. It replaces an installed marketplace plugin silently (the marketplace row still shows enabled; only the--debuglog in~/.claude/debug/recordsPlugin "<name>" from --plugin-dir overrides installed version). It replaces a skills-directory plugin with a Errors row explaining the name is taken by a session-only plugin. - An installed marketplace plugin. A same-named skills-directory plugin gets a similar "not loaded" row.
- A skills-directory plugin. Between two of these,
~/.claude/skills/beats the project's.claude/skills/, with a row naming the path that shadowed it. - A synced plugin. Any other enabled plugin with the same name wins and the synced copy is reported as not loaded. Disable your own copy to use the claude.ai one.
Comparison uses manifest names, so a --plugin-dir plugin whose manifest says "name": "release-notes" replaces release-notes@acme-plugins if that plugin's manifest also says release-notes, even if the folder is called something else.
To stop a session-only plugin shadowing anything (handy when a wrapper script passes --plugin-dir for you), disable it in any settings file, for example "enabledPlugins": { "release-notes@inline": false }. A disabled session-only plugin shadows nothing, so the installed copy loads.