Skip to content

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.

StageWhere it livesWhat it contains
DeclaredSettings filesenabledPlugins 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
LoadedThe running sessionWhatever 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.json gets 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 suffixHow it got thereHow 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 sessionOn, unless the manifest has defaultEnabled: false or settings set "<name>@inline": false
@skills-dirA 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
@syncedTurned on for your claude.ai account (by you or your organisation) and downloaded by Claude CodeOn, 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 -p is 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_TOKEN or an apiKeyHelper supplies the credential;
  • the session does not fetch feature flags (for example with CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC set; see Environment variables);
  • the session is in bare mode or started with --safe-mode;
  • --setting-sources omits user.

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": false to user settings. Put the same key in the committed project settings to keep it out of a repo everywhere.
  • All synced plugins: set syncClaudeAiPlugins to false in 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 disable refuses with a message saying it is required by your organisation, and claude plugin list labels it required by your org.

Finding where a plugin is enabled

enabledPlugins can be set in six sources. From lowest to highest precedence:

SourceFileApplies to
--add-dir.claude/settings.json or .claude/settings.local.json in a directory passed with --add-dirThis session only. Only true has any effect, and every other source beats it
user~/.claude/settings.jsonYou, everywhere
project.claude/settings.jsonEveryone who clones the repo
local.claude/settings.local.jsonYou, in this repo
flagThe file passed to --settingsThis session only
managedManaged settingsEveryone 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.

PathContents
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.jsonThe install and marketplace records. Marketplaces hosted on claude.ai go in known_marketplaces_claudeai.json instead
flagged-plugins.jsonPlugins uninstalled because their marketplace delisted them; shown under Flagged in /plugin
installed_plugins.set-aside.<date>.<hash>.json, installed_plugins.unreadable.<date>.<hash>.keptDated 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 pluginHow it loads
--plugin-dir and skills-directory pluginsIn 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 pathIn 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 modeIn place, through links in the cache entry
Every other marketplace pluginCopied 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:

LockfileTool
bun.lockBun
npm-shrinkwrap.jsonnpm
package-lock.jsonnpm

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, .env or bunfig.toml. On success, node_modules is moved into the plugin.
  • Resolution is frozen to the lockfile, and the install is skipped if package.json and the lockfile disagree.
  • --ignore-scripts is used, so native modules that build in postinstall download but are not compiled.
  • npm overrides (with an npm lockfile) or Bun patchedDependencies (with bun.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:

  1. version in the plugin's manifest, if set.
  2. Otherwise version on the marketplace entry.
  3. Otherwise, derived from the source:
SourceFallback version
github, url, git-subdirSource commit SHA, first 12 characters. git-subdir adds a hash of the subdirectory path
archiveSHA-256 (12 characters) from the entry's sha256 pin, or of the downloaded file if unpinned
Relative path in a git-hosted marketplaceCommit SHA of the installed folder
Local folder where neither plugin nor marketplace is a git repounknown
npmunknown

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 typeWhereRefreshed
name@marketplace/plugin install or claude plugin installThat marketplace, before lookup
name only/plugin installOnly auto-updating marketplaces, and only after a miss
name onlyclaude plugin installNothing; 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:

  1. autoUpdate on its extraKnownMarketplaces entry in a settings file.
  2. autoUpdate in known_marketplaces.json, written by Enable auto-update under /plugin > Marketplaces (which also writes to the settings entry if one exists).
  3. The default: on for Anthropic's official marketplaces such as claude-plugins-official, off for knowledge-work-plugins and first-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-plugins if 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:

  1. A plugin whose id appears in managed enabledPlugins (either value). A --plugin-dir copy with a matching manifest name is not loaded, with the message --plugin-dir copy of "<name>" ignored: plugin is locked by managed settings.
  2. A session-only plugin from --plugin-dir, --plugin-url or CLAUDE_CODE_PLUGIN_DIRS. It replaces an installed marketplace plugin silently (the marketplace row still shows enabled; only the --debug log in ~/.claude/debug/ records Plugin "<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.
  3. An installed marketplace plugin. A same-named skills-directory plugin gets a similar "not loaded" row.
  4. 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.
  5. 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.