Skip to content

Plugin lifecycle and configuration

Claude Code plugins are add-on bundles that can contribute skills/commands, agents, hooks, MCP and LSP servers, output styles, themes, workflows, monitors, and a small allowlisted settings slice. A plugin is not one configuration record: marketplace declaration, downloaded installation state, enablement, manifest metadata, plugin-owned defaults, and user-supplied options are separate layers with different stores and precedence rules.

This page describes the local plugin lifecycle in @anthropic-ai/claude-code@2.1.215. Use MCP, plugins, and hooks for marketplace acquisition and MCP/hook execution details, and Settings, policy, and integrations for the general settings merge.

Short answer

flowchart TD
Declare[extraKnownMarketplaces declaration] --> Materialize[known_marketplaces.json + marketplace cache]
Materialize --> Install[versioned plugin cache + installed_plugins.json]
Install --> Enable[enabledPlugins by scope]
Manifest[plugin.json / marketplace entry] --> Install
Enable --> Load[loadAllPlugins]
Load --> Contributions[skills / agents / hooks / MCP / LSP / UI additions]
Load --> Defaults[allowlisted plugin-owned defaults]
Options[userConfig values] --> Expand[config substitution / child env]
Secrets[secure storage] --> Expand
Expand --> Contributions

The important separations are:

LayerPrimary stateWhat it means
Marketplace declarationextraKnownMarketplaces in settingsA scope declares where a named marketplace comes from.
Marketplace materializationknown_marketplaces.json plus local cacheClaude Code acquired and validated a marketplace source.
Installationversioned cache plus installed_plugins.jsonOne plugin version exists at user/project/local/managed installation scope.
EnablementenabledPluginsThe effective settings cascade decides whether that installed or discoverable plugin is active.
Manifest.claude-plugin/plugin.json or marketplace entryDeclares components, dependencies, defaults, and option schemas.
Plugin-owned defaultsplugin settings.json or manifest settingsLow-precedence defaults for a small allowlist; not arbitrary settings injection.
User optionspluginConfigs plus secure storageValues for manifest userConfig; distinct from enablement and plugin-owned defaults.

Source anchors

Semantic aliasApproximate location in cli.renamed.jsExact string or symbolMeaning
PluginManifestSchema~68,800–70,100_At(), userConfig, defaultEnabled, dependenciesCanonical plugin manifest and configurable-option schema.
EnabledPluginsSchema~71,119enabledPluginsScoped enable/disable and extended version-constraint setting.
PluginConfigsSchema~71,325pluginConfigs, options, mcpServersNon-sensitive persisted plugin and channel/MCP option values.
PluginOptionResolver~227,482MGr(), ApgMerges plugin options only from user, flag/SDK, and policy settings.
PluginOptionStorage~253,730fJt(), mJt(), pluginSecretsSplits non-sensitive options into user settings and sensitive options into secure storage.
PluginSubstitution~253,845Fxt(), lQn(), CLAUDE_PLUGIN_OPTION_Runtime expansion and sensitive-content filtering.
PluginMcpConfiguration~281,090–281,310skg(), lkg(), Keo()Combines defaults/options and expands plugin MCP definitions.
PluginLspConfiguration~317,269–317,420Zkt(), HFg(), UCu()Loads, validates, expands, and namespaces plugin LSP servers.
BackgroundPluginRefresh~952,596–953,070CLAUDE_CODE_ENABLE_BACKGROUND_PLUGIN_REFRESHLets the stream/headless loop reconcile deferred plugin state before later command drains.
PluginHookHotReload~334,264getPluginAffectingSettingsSnapshot(), setupPluginHookHotReload()Managed-policy changes can invalidate and reload plugin hooks.
PluginInstaller~470,000–470,620kJr()Resolves dependency closure, writes enablement, materializes caches, and attempts rollback on failure.
PluginManifestLoader~471,592loadPluginManifest()Reads and validates .claude-plugin/plugin.json, with marketplace fallback behavior.
PluginAssembler~471,900–473,730createPluginFromPath(), mergePluginSources(), loadAllPlugins()Resolves component paths, source precedence, partial errors, and enabled/disabled sets.
PluginDefaultSettings~473,570Dgy(), cachePluginSettings(), cndLoads allowlisted plugin defaults before ordinary settings layers.
PluginCliConfig~654,099--config, oS_(), iS_()Validates repeated KEY=VALUE options after CLI installation.
PluginEvalSchemas~657,872–658,177case.yaml, prompt.md, graders/*.mdParses evaluation cases, execution controls, and grader definitions with caps.
PluginEvalSandbox~658,275–658,469cwd/, config/, home/, out/, EVAL_*Creates per-run isolated directories and a restricted case-provided environment.
PluginEvalRunLoop~659,462–659,736with-without, scaffold_script, dontAskRuns arms/cases, grades results, enforces cost/timeout, and cleans temporary state.
PluginEvalReports~659,738–660,629aggregate-result.json, HTML report, Artifact publishProduces aggregate, full JSON, local HTML, and optional private hosted reports.
PluginEvalRegistration~660,841–660,964plugin eval [target], plugin eval init [name]Registers the early-access runner and suite-authoring flow.
PluginInteractiveConfig~795,996–802,400/plugin configure, fJt()Interactive option editor using the same storage split.

Discovery and source precedence

loadAllPlugins() assembles four source families:

  1. Session-only plugins from --plugin-dir, --plugin-url, and host-synced directories.
  2. Installed marketplace plugins resolved from settings, marketplace state, and the versioned cache.
  3. Skills-directory plugins discovered from user/project skill roots that contain plugin metadata.
  4. Built-in plugins registered by the runtime.

mergePluginSources() applies name-level collision rules before component loading:

  • a managed plugin name blocks a same-name session-only plugin;
  • a session-only plugin overrides an installed marketplace plugin with that name;
  • an installed or session-only plugin shadows a same-name skills-directory plugin;
  • user skills-directory plugins shadow same-name project copies;
  • project skills-directory plugins are suppressed until workspace trust is accepted.

This is plugin-source precedence, not settings precedence. Once the plugin list is assembled, enabledPlugins and dependency checks decide which records are active.

Marketplace, installation, and enablement

Marketplace declaration/materialization is documented in Plugin marketplace lifecycle. Installing a plugin adds another two layers:

sequenceDiagram
participant CLI as /plugin or claude plugin
participant Resolver as dependency resolver
participant Settings as enabledPlugins scope
participant Cache as versioned plugin cache
participant State as installed_plugins.json
CLI->>Resolver: install plugin@marketplace at scope
Resolver->>Resolver: policy + dependency + version checks
Resolver->>Settings: write root/dependency enablement
Resolver->>Cache: acquire and validate closure
Cache->>State: record scope/path/version/auto metadata
alt acquisition fails
Resolver->>Settings: best-effort rollback
end

Install scopes

The editable installation scopes are user, project, and local:

ScopeSettings sourceInstallation identity
user~/.claude/settings.jsonShared across projects for the user.
project.claude/settings.jsonTeam-shared enablement for the current project.
local.claude/settings.local.jsonPrivate override for the current project.

The installation registry can also contain managed entries, but CLI mutation rejects managed scope. Project/local entries carry a projectPath keyed to the original working directory, so one user can retain installations of the same plugin for different project directories. Scope resolution prefers a local entry matching the current original cwd, then a matching project entry, then a user entry.

enabledPlugins

Ordinary entries use plugin@marketplace keys:

{
"enabledPlugins": {
"formatter@company-tools": true,
"legacy-helper@company-tools": false
}
}

For unrestricted top-level keys, the normal settings order is user → project → local → flag/SDK → policy. Therefore a project true overrides a user false, a local false can override the project for one user, and managed policy is not locally overridable. The runtime reports ineffective lower-scope disables and points at the winning source.

The schema also accepts arrays of strings as an extended representation for dependency version constraints. Those arrays are not equivalent to true; dependency resolution and autoupdate use them as pinner input. Ordinary users should use the plugin commands rather than hand-authoring that internal shape.

defaultEnabled applies only when no explicit setting exists. defaultEnabled:false leaves the plugin installable/discoverable but does not activate it merely because it was installed. An enabled dependent can make a default-disabled dependency active when no explicit disable overrides it; an explicit higher-scope false blocks dependency activation and is reported to the user. Disabling a plugin is blocked while active reverse dependencies still require it unless the coordinated bulk-disable path handles the whole set.

Installation is recoverable, not one transaction

The resolver writes the intended enabledPlugins closure before all downloads finish so dependency state is explicit. If acquisition fails it attempts to restore the prior values. A failed rollback is logged with manual recovery guidance. Versioned cache/state writes and settings writes are therefore coordinated but not one crash-atomic transaction.

Auto-installed dependencies carry auto: true; plugin prune removes only auto-installed entries no longer reachable from an enabled root. Manual installs are preserved.

Manifest and component loading

loadPluginManifest() first checks .claude-plugin/plugin.json. Strict marketplace entries require a valid manifest; non-strict entries can supply canonical metadata themselves. Every component path is resolved below the plugin root, and traversal or missing paths become structured plugin errors rather than arbitrary filesystem reads.

Implicit versus explicit component paths

ComponentImplicit sourceExplicit behavior
Commandscommands/Manifest commands replaces the implicit directory scan; values can be paths or inline metadata/content.
Agentsagents/Manifest agents replaces the implicit directory scan.
Skillsskills/ and, in supported layouts, root SKILL.mdExplicit skill directories are generally additive, with marketplace-root special cases.
Hookshooks/hooks.jsonManifest hooks are added; duplicate references to the implicit file are detected.
MCP serversroot .mcp.jsonManifest mcpServers is merged after the root file.
LSP serversroot .lsp.jsonManifest lspServers is merged after the root file; see IDE integration and LSP diagnostics.
Output stylesoutput-styles/Explicit field replaces implicit directory loading.
Themesthemes/Explicit field replaces implicit directory loading.
Workflowsworkflows/Explicit field replaces implicit directory loading.
Monitorsmonitors/monitors.jsonManifest/experimental monitor declaration replaces the implicit file.
Plugin defaultsroot settings.jsonValid allowlisted file values win over manifest settings; malformed file input falls back to manifest values.

A failure in one plugin or component does not necessarily erase every other plugin. The loader returns enabled/disabled records alongside errors and warnings, emits a partial-failure outcome when appropriate, and only classifies the whole load as failed when no plugin survives.

Plugin evaluation (early access)

claude plugin eval [target] is a dedicated evaluation runner rather than a normal loaded plugin component. The command tree is registered in this build, but both eval actions call an early-access guard before doing work. When tengu_walnut_spire/CLAUDE_CODE_WALNUT_SPIRE does not enable the feature, invocation immediately reports that plugin eval is in early access.

The target can be a path, a bare plugin name, or plugin@marketplace. Installed and skills-directory plugin identities resolve through the plugin registries; an ambiguous bare name fails with the candidate list. Default ablation is with-without when target resolution establishes a plugin identity and none for an ordinary path. A plain skills-style path containing only SKILL.md is not automatically discovered as a plugin while walking upward for a manifest, so callers should not assume that every path gets a meaningful no-plugin baseline.

Case and grader formats

The discovery root can contain either:

evals/**/case.yaml

or the prose form:

evals/**/prompt.md
evals/**/graders/*.md

case.yaml supports metadata/tags/plugin paths, context setup, execution prompt/history/additional directories, model/tool/system/env controls, run count, graders, and expected outcome. Important bounds are:

ControlDefaultMaximum
runs350
execution.max_turns10200
execution.timeout_seconds3003,600
One prose case/grader file1 MiB
Grader files for one prose case256

Schema version is a string and this runner accepts the 1.x major. A case needs either an execution prompt or a history file and at least one grader. Supported grader types are regex, tool_order, tool_used, file_exists, llm, and baseline; weighted pass/fail results become each run’s score.

Explicit plugins: paths are realpathed and must remain below the discovery root. Without that field, the loader walks upward for plugin.json or .claude-plugin/plugin.json. In with-without mode, each valid case executes once with its resolved plugin directories and once with none. A case with no resolved plugin is excluded rather than reporting a meaningless delta. Plugin-activation tool_used checks can be display-only under ablation so the headline score measures outcome graders rather than mechanically rewarding invocation.

Execution sandbox and trust boundary

Each run receives a temporary tree with cwd/, config/, home/, and out/. The child receives isolated HOME/USERPROFILE, XDG and Claude config roots, a managed-settings path, disabled update/nonessential traffic controls, and copied credentials when available. Case-provided environment keys are accepted only with the EVAL_ prefix.

The effective tool grant combines the case’s allowed_tools with operator --allow-tools, while a small read/task support set remains available to the harness. The evaluation child runs with --permission-mode dontAsk; it cannot open normal interactive approval prompts. Global --model overrides per-case models, --judge-model controls LLM grading, and the tool process is force-killed when its timeout expires.

context.scaffold_script is a deliberate exception to the sandbox story. It is off by default and runs only with --scaffold; when enabled, author-supplied Bash runs as the operator. A nonzero scaffold exit gives that run a zero/error result. The warning in the CLI is accurate: only opt in for suites authored by the operator or organization. --no-scaffold makes the default explicit.

Temporary directories are removed after a normal run. --keep-temp preserves them; non-JSON mode also preserves failed-run directories for diagnosis. The sandbox credential copy is removed during cleanup, but this source cannot guarantee erasure from storage media.

Cost, interruption, reports, and exits

--max-cost-usd is suite-wide. Once the budget is breached, no later agent run begins; paid LLM/baseline graders on the breaching run are marked failed/skipped while free graders still execute. Because the check follows an agent run, overrun is bounded to one agent run rather than zero. The aggregate is marked partial with reason cost_ceiling and exits 2.

The first SIGINT aborts active work, prints an interruption notice, and still assembles partial output from completed runs. After cleanup/reporting, interruption exits 130. Ordinary threshold or case-load failure exits 1; a complete suite meeting the threshold exits 0. Individual errored runs score zero but do not necessarily stop all remaining cases.

Every run directory can produce aggregate-result.json. Additional outputs are:

OptionResult
--jsonFull normalized suite/case/arm/run/grader result on stdout.
--json <file.json>Writes that full result to the named file.
--report <path>Self-contained HTML report with prompts, graders, evidence, scores, and ablation delta.
--publish-reportPrivately publishes the HTML through Artifact when account/policy/provider/privacy gates allow it; otherwise explains why and suggests a local report.

claude plugin eval init [name] is the authoring companion. --bare creates a blank prompt.md plus graders/criteria.md; the normal interactive route launches a guided interview that inspects the plugin and helps design/calibrate cases. That interview prompt recommends practices, but executable schema/runner behavior—not the generated prose—is the authority for this page.

The manifest schema also exposes experimental.evals. The traced CLI runner above discovers filesystem cases below the resolved target; this page does not claim that the manifest field independently redirects every discovery call without a separate executable path.

Plugin-owned default settings

Plugins cannot inject arbitrary settings through their manifest. In this artifact Dgy() validates plugin settings.json or manifest settings against a pick-list built from:

agent
subagentStatusLine

Enabled-plugin defaults are aggregated by cachePluginSettings(). If two enabled plugins supply the same key, the later record in the assembled plugin list wins and a debug warning is emitted. The resulting object is inserted before user/project/local/flag/policy settings in SSi(), so ordinary settings override it.

This layer is distinct from pluginConfigs:

  • plugin-owned defaults change an allowlisted Claude Code setting while the plugin is enabled;
  • pluginConfigs stores values the user supplied for that plugin’s own userConfig schema.

userConfig: plugin-specific options

A plugin manifest can declare named options with these fields:

FieldMeaning
typestring, number, boolean, directory, or file.
title, descriptionUI label and help text.
requiredEmpty/missing values fail validation.
defaultString, number, boolean, or string-array default.
multipleAllows an array for a string option.
sensitiveMasks input and stores the value outside settings JSON.
min, maxBounds numeric options.

Option keys must match ^[A-Za-z_]\w*$ because hooks receive corresponding CLAUDE_PLUGIN_OPTION_<KEY> environment variables.

Resolution and storage

MGr(pluginId) merges only these settings sources, in order:

userSettings → flagSettings → policySettings

Project and local pluginConfigs are deliberately absent from this resolver. This prevents repository-controlled settings from supplying plugin secrets/options. Secure-storage values are merged last and override settings values.

The interactive /plugin configure <plugin> flow and CLI installation’s repeatable --config KEY=VALUE use the same validator/storage split:

Value classPersistence
Non-sensitive plugin optionuserSettings.pluginConfigs[pluginId].options
Sensitive plugin optionpluginSecrets[pluginId] in secure storage
Non-sensitive channel/MCP optionuserSettings.pluginConfigs[pluginId].mcpServers[server]
Sensitive channel/MCP optionpluginSecrets[pluginId/server] in secure storage

Both interactive and install-time option writes persist the non-sensitive half to user pluginConfigs; there is no project/local option-write mode. That matches MGr()’s user → flag/SDK → policy read path. Project/local scoping applies to plugin installation/enablement, not to userConfig values.

When a schema changes from sensitive to non-sensitive or the reverse, save paths scrub stale copies from the wrong store. Uninstalling the final installation removes plugin options/secrets and normally removes the plugin data directory.

Example persisted non-sensitive state:

{
"pluginConfigs": {
"deploy@company-tools": {
"options": {
"REGION": "us-east-1"
}
}
}
}

A sensitive TOKEN option would not appear in that JSON.

Missing and defaulted values

Runtime option reads combine stored values with secure storage. Component-specific helpers then add schema defaults. Required options without defaults remain missing and can produce a configure prompt, skip an MCPB server, or attach a component configuration error. Installing the plugin itself can still succeed; “installed” does not guarantee that every configurable component is runnable.

Where option values are exposed

ConsumerBehavior
MCP server configExpands ${user_config.KEY} in command, args, env, URL, and headers after plugin-root/data variables. Missing env/config values produce structured config errors. A shell-based headersHelper may not interpolate user_config directly.
LSP server configExpands ${user_config.KEY} in command, args, env, and workspace folder; reports missing environment variables.
Command hooksExec-form fields can substitute user config. Shell-form commands that reference ${user_config.*} are rejected because the value would be reparsed by a shell. Every option is additionally exported as a normalized CLAUDE_PLUGIN_OPTION_<KEY> child variable.
Skills and agentsNon-sensitive options may be substituted into content. Sensitive fields become a literal “sensitive option … not available” placeholder.
MonitorsDirect ${user_config.*} references are rejected because monitor commands are shell strings; the script must read from a safer configuration channel.

CLAUDE_PLUGIN_ROOT, CLAUDE_PLUGIN_DATA, and CLAUDE_PROJECT_DIR are separate runtime variables. CLAUDE_PLUGIN_DATA points to a per-plugin local data directory and is not the same store as pluginConfigs or secure storage.

Reload and activation boundaries

Different changes have different activation costs:

ChangeCurrent-session behavior
User/project/local enabledPlugins editAn accepted settings event refreshes the effective settings/app state, but the automatic plugin-hook subscriber ignores non-policy sources and no full plugin contribution rebuild follows. Run /reload-plugins or restart.
Managed plugin-affecting policy changeOn a policySettings event, setupPluginHookHotReload() compares effective enabledPlugins/extraKnownMarketplaces plus policy strictKnownMarketplaces, blockedMarketplaces, and disableSideloadFlags; a changed snapshot invalidates plugin caches and reloads plugin hooks. User/project/local declaration events and known_marketplaces.json materialization do not trigger this path.
/plugin configure option saveOption caches are cleared immediately. Components that were already instantiated may still require /reload-plugins, MCP reconnect, or restart according to their owner.
/reload-plugins [--force]Rebuilds plugin cache view, commands/skills, agents, hooks, MCP and LSP descriptors, and can repair eligible missing dependencies.
/reload-skillsRe-enumerates skills only; it does not rebuild the full plugin contribution set.
Plugin updateCLI output says restart to apply the new version.
Deferred startup install/state change with CLAUDE_CODE_ENABLE_BACKGROUND_PLUGIN_REFRESH=trueBefore later stream/headless command-queue drains, the loop clears a needsRefresh/late-install marker and runs the plugin-state refresh owner. Failure is logged and the queue continues. This is conditional reconciliation, not periodic marketplace polling or a complete contribution hot reload.

The full reload command first checks whether changing plugin MCP tool names would invalidate prompt-cache assumptions. Without Tool Search protection, it can require --force after conversation output exists.

Trust, policy, and safe mode

  • Workspace trust gates project/local marketplace declarations and project skills-directory plugins.
  • Safe mode skips plugin loading and registration; managed settings-file policy still applies separately.
  • strictKnownMarketplaces and blockedMarketplaces constrain marketplace sources before acquisition/use.
  • disableSideloadFlags rejects --plugin-dir and --plugin-url rather than letting flags bypass marketplace policy.
  • strictPluginOnlyCustomization can require selected customization families to come from plugins or managed/built-in sources.
  • Managed enabledPlugins values win over lower settings scopes. A local setting cannot disable a plugin that policy explicitly enables.
  • Plugin paths, component declarations, local copies, Git SHAs, archives, and cache paths have containment/integrity checks, but plugin code and hooks remain executable local extensions and should be treated as trusted code.

Failure and cleanup behavior

FailureResult
Invalid/corrupt plugin.jsonStructured manifest load error; strict plugin is not loaded.
One component path escapes the rootThat component records a traversal error; other valid components can survive.
Missing required userConfigConfiguration UI/error or affected server suppression; plugin installation can remain.
Dependency cycle/range conflictInstallation or enable operation stops with the dependency chain/range reason.
Cross-marketplace dependency not allowedAutomatic dependency installation stops or requires manual install; only the root marketplace’s allowlist grants cross-marketplace trust.
Cache acquisition failsTemporary cache is cleaned when possible; enablement rollback is attempted.
Plugin is disabled while dependents need itOperation is blocked unless a coordinated bulk path disables dependents too.
Autoupdate violates a pinner constraintUpdate is held and reports the plugins supplying the constraint.
Final uninstallRemoves the scoped enable/install record; when no installation remains, removes cache, options/secrets, usage state, and normally plugin data.

Boundaries and caveats

  • Plugin loader/source order and manifest symbols are build-specific implementation anchors, not public APIs.
  • Marketplace declaration/materialization and plugin installation are related but distinct transactions; see the marketplace page for acquisition details.
  • Plugin option secure storage is abstracted behind the runtime credential store. This page establishes split/merge behavior, not OS keychain internals.
  • Component activation is not one universal hot-reload operation; use the owner-specific reload/reconnect path.
  • Hosted ListPlugins/SearchPlugins/SuggestPluginInstall operate on claude.ai catalogs and cards. They are separate from the local marketplace/cache/plugin lifecycle described here.
  • Eval suites are not loaded into ordinary sessions as plugin contributions. plugin eval is a gated CLI harness with its own execution and report boundaries.
  • Eval scaffold_script runs operator-authored Bash outside the child run sandbox and should be treated as trusted executable code.
  • source-atlas/ was intentionally not regenerated for this focused trace; enclosing control flow in the retained readable bundle supplied direct evidence.

Created and maintained by Yingting Huang.