Skip to content

Plugin Dependency Declaration and Disable-Chain Hints

Plugins declare dependencies in their manifest; the harness validates them at install, refuses to disable a plugin another enabled one needs, and prunes orphaned auto-installs.

A flat plugin set duplicates shared skills, MCP servers, and hooks. Plugin dependency declaration is the next layer on top of plugin packaging: a dependencies array in plugin.json plus host rules for install, enable, disable, and prune. Claude Code v2.1.143 (2026-05-15) is the reference implementation. It validates dependencies, auto-installs transitive dependencies, refuses disable with a hint, and removes orphans on prune (Claude Code changelog).

When the dependency graph earns its complexity

A dependency edge adds error surface: range-conflict, dependency-version-unsatisfied, no-matching-tag, and cross-marketplace (Constrain plugin dependency versions). It pays back when the set is large enough that duplication is a real cost (roughly five plugins and up), upstream follows semver, and marketplaces are reachable. Otherwise a flat set is cheaper.

Declaring a dependency

Dependencies live in the dependencies array of .claude-plugin/plugin.json. Each entry is a bare plugin name or an object with name, version (any semver range), and optional marketplace (Constrain plugin dependency versions):

{
  "name": "deploy-kit",
  "version": "3.1.0",
  "dependencies": [
    "audit-logger",
    { "name": "secrets-vault", "version": "~2.1.0" }
  ]
}

Version constraints resolve against git tags named {plugin-name}--v{version}. The command claude plugin tag --push derives and pushes the tag from the manifest (Constrain plugin dependency versions). Cross-marketplace dependencies are blocked unless the root marketplace lists the target in allowCrossMarketplaceDependenciesOn. Trust does not chain through intermediate marketplaces.

The disable-chain hint

The operator sees a refusal, not a warning. When claude plugin disable A would orphan an enabled B that depends on A, Claude Code refuses and prints a copy-pasteable chain hint (Claude Code changelog v2.1.143). The matching verb is force-enable: claude plugin enable B walks the graph and enables A.

graph LR
    O[claude plugin disable A] --> H{enabled B depends on A?}
    H -->|yes| R[refuse + print disable-chain hint]
    H -->|no| D[disable A]
    E[claude plugin enable B] --> T[force-enable transitive deps]

Refusal forces a choice: the operator disables the dependent plugin first or abandons the action. A dismissed warning would leave the dependent plugin half-broken, with its dependency record pointing at a disabled record.

Pruning orphaned auto-installs

claude plugin prune (v2.1.121, aliased autoremove) removes auto-installed dependencies that no installed plugin requires; user-installed plugins are never pruned (Plugins reference — plugin prune). Pass --prune to plugin uninstall to cascade.

Error code Meaning Fix
dependency-unsatisfied Declared dep not installed or disabled Run the claude plugin install shown in the message
range-conflict Two plugins' semver ranges do not intersect Update one of the conflicting plugins, or widen the upstream range
dependency-version-unsatisfied Installed dep is outside the declared range claude plugin install <dependency>@<marketplace> to re-resolve
no-matching-tag No {name}--v* tag satisfies the range Tag upstream with claude plugin tag or relax the range

Errors surface in claude plugin list, /plugin, and /doctor; programmatic checks consume claude plugin list --json (Constrain plugin dependency versions).

Why it works

The host harness owns the registry of every component a plugin contributes. A skill is a record the harness consults on every prompt, not a file the user sources. Because the harness owns the registry, the same lookup that resolves a skill on invocation can walk the dependency graph at disable time. Install-time provenance lets prune tell safe-to-remove from off-limits (Constrain plugin dependency versions). This is apt autoremove against dpkg's database (Linux Journal), applied to agent capabilities.

When this backfires

  • Small flat plugin sets: under five plugins, the graph adds error surface without saving real duplication, and the chain hint never fires.
  • High-churn upstream without semver: when an upstream force-moves a tag or treats minor bumps as breaking, downstream plugins thrash between dependency-version-unsatisfied and no-matching-tag (plugin dependencies).
  • Federated marketplaces without governance: allowCrossMarketplaceDependenciesOn needs the root maintainer to allowlist each target, so without a coordinator every cross-marketplace edge becomes a manual install.
  • Always-on token budget pressure: every transitive plugin loads skill and agent descriptions into the always-on context, so check per-plugin token-cost attribution before adding an edge.
  • Air-gapped installs: resolution assumes the marketplace is reachable, so when it is not, missing transitive dependencies disable the dependent plugin the operator never touched.
  • Dependency hell: importing the package-manager primitive imports its failure modes, and the four error codes above are the agent-layer equivalent of package-manager pain.
  • Expanded supply-chain attack surface: force-enabling transitive dependencies and auto-installing them from a marketplace pulls every upstream maintainer into your trust boundary. A compromised marketplace skill can hijack the install and ship a trojanized dependency that imports cleanly while exfiltrating secrets (SentinelOne: Marketplace Skills and Dependency Hijack in Claude Code). Vet marketplace provenance and prefer pinned ranges over open auto-update.

Example

A platform team publishes secrets-vault, an MCP server wrapping a secrets backend. A deploy team publishes deploy-kit, which calls secrets-vault during deploys and is tested against secrets-vault v2.1.0 (Constrain plugin dependency versions).

Before, with flat plugins and no declared dependency:

# Platform team tags secrets-vault--v2.2.0 with a renamed MCP tool.
# Auto-update moves every engineer's secrets-vault to v2.2.0.
# deploy-kit silently breaks on the next deploy.

After, with a declared dependency and a version constraint:

{
  "name": "deploy-kit",
  "version": "3.1.0",
  "dependencies": [
    { "name": "secrets-vault", "version": "~2.1.0" }
  ]
}

Engineers with deploy-kit installed stay on the highest matching 2.1.x patch. Auto-update fetches secrets-vault--v2.1.x, not --v2.2.0. If an engineer runs claude plugin disable secrets-vault, Claude Code refuses with a hint pointing at deploy-kit:

Error: cannot disable secrets-vault — deploy-kit (enabled) requires it.
To proceed, first disable: deploy-kit
  claude plugin disable deploy-kit

When deploy-kit is later uninstalled, claude plugin uninstall deploy-kit --prune removes the auto-installed secrets-vault too — provided no other installed plugin still depends on it.

Key Takeaways

  • Plugin dependency declaration is a dependencies array in plugin.json with optional semver ranges and cross-marketplace fields
  • Host enforcement: disable refuses with a copy-pasteable chain hint, enable force-enables transitive deps, prune removes orphaned auto-installs
  • The primitive works because the host harness owns the registry of every component a plugin contributes — refusal and prune use the same lookup that drives invocation
  • Earned only when the plugin set is large enough, upstream follows semver, and marketplaces are reachable — small flat sets pay the error-surface cost without the deduplication benefit
  • Cross-link with per-plugin token-cost attribution before adding edges — transitive plugins compound always-on cost
Feedback