Claude Code Plugin Dependencies and Version Constraints

Use an object entry in the dependencies array of the plugin's manifest, with a name and a semver range, for example a tilde range that allows only patch updates. Without a range the dependency tracks whatever version its marketplace currently provides, so an upstream release can change it under you without warning.
Constraints resolve against git tags on the repository hosting the dependency, named with the plugin name, two dashes, a v, and the version. If the upstream never tags releases, no tag satisfies the range and the install fails with a no-matching-tag error. For npm, archive and command sources the range is only checked at load time, never used to choose a version.
A command that creates the release git tag Claude Code needs to resolve version constraints. Run it from the plugin directory and it derives the tag from the manifest and the marketplace entry, refusing unless the plugin validates, the two versions agree, the working tree is clean, and the tag does not already exist. Add the push flag to push it to origin.
Yes. Besides the required name, a plugin manifest can consist of nothing but a dependencies array, so installing it installs every plugin it lists. Platform teams use this to publish role-specific standard sets, and adding a tool later means publishing a new bundle version rather than asking everyone to install something.
Run the plugin prune command, which lists auto-installed dependencies that no plugin still requires and removes them after confirmation. Plugins you installed yourself are never pruned. You can also pass the prune flag to an uninstall so the cleanup happens in the same step, and a dry-run flag shows what would go without removing anything.

Key Takeaway
A Claude Code plugin can depend on other plugins, and by default a dependency tracks the latest available version, so an upstream release can change it under you without warning. A semver range holds it at a tested version, resolved against git tags named plugin-name double-dash v version on the repository that hosts the dependency.
Someone on the platform team renamed a tool inside an MCP server, tagged a release, and by lunchtime three deploy pipelines were failing with an error that named a tool nobody had touched. Nothing was wrong with either plugin. The dependency between them had no version constraint, so an upstream release moved under every engineer at once.
That failure has a one-line fix and a set of mechanics worth learning properly, because half of them are invisible until they misfire. This post covers what constraints do, how to declare them, the git tag convention that makes resolution work at all, bundling a curated set behind one install, crossing marketplace boundaries, how several constraints on the same dependency combine, and the four errors you will actually see.
Picture an internal marketplace with two teams on it. The platform team maintains secrets-vault, an MCP server wrapping a secrets backend. The deploy team maintains deploy-kit, which calls it during deploys and is tested against version 2.1.0. With no constraint, the next release that renames a tool moves everyone's secrets-vault and breaks deploy-kit everywhere at the same moment.
With a constraint, deploy-kit declares that it needs the 2.1.x range and engineers stay on the highest matching patch. The deploy team then upgrades on their own schedule by publishing a deploy-kit release with a wider range. The point is not that upgrades are bad — it is that the team who has to fix the breakage should be the team who chose the timing.
Dependencies live in a dependencies array in the plugin's own manifest, or in its marketplace entry. An entry can be a bare string, meaning whatever version that plugin's marketplace provides, or an object with a name, an optional semver range, and an optional marketplace when the same plugin name exists in more than one catalogue.
// .claude-plugin/plugin.json
{
"name": "deploy-kit",
"version": "3.1.0",
"dependencies": [
"audit-logger", // any version
{ "name": "secrets-vault", "version": "~2.1.0" },
{ "name": "shared-utils", "marketplace": "acme-shared" }
]
}
// Ranges are npm semver:
// 1.2.3 exactly that
// ~2.1.0 2.1.0 up to 2.1.x
// ^2.1.0 2.1.0 up to 2.x.x
// >=1.4 anything at or above
// * anything
// Pre-releases are EXCLUDED unless the range opts in: ^2.0.0-0The constraint controls which version is fetched only for git-backed sources. For an npm, archive or command source it is checked at load time instead, and the dependent plugin is disabled with dependency-version-unsatisfied when the installed version falls outside the range. There is one sharp edge in that: a command-source dependency whose plugin.json sets no version satisfies no constraint at all, so set a version before you constrain it.
This is the part people miss, and without it constraints silently do nothing useful. Claude Code resolves a range against git tags on the repository that hosts the dependency — the plugin's own repository for github, url and git-subdir sources, or the marketplace repository for a plugin referenced by relative path. The tag name is a fixed convention, and there is a command that gets it right for you.
# Constraints resolve against GIT TAGS on the repository that
# hosts the dependency. The convention is not optional:
#
# {plugin-name}--v{version}
#
# From the plugin directory:
claude plugin tag --push
claude plugin tag --dry-run # what would be tagged
claude plugin tag --remote upstream # push somewhere else
# It refuses to tag unless:
# - the plugin contents validate
# - plugin.json and the marketplace entry agree on the version
# - the working tree under the plugin directory is clean
# - the tag does not already exist
# Success looks like:
# Created tag secrets-vault--v2.1.0
# Pushed to origin
# No matching tag, and the install fails with:
# Dependency "secrets-vault@acme-tools" has no git tag
# satisfying ~2.1.0Besides the required name, a manifest can consist of nothing but dependencies — which turns a plugin into a bundle. A platform team can publish role-specific sets so a new engineer runs one install instead of five, and adding a tool to the standard set later is a new bundle release rather than a message asking everyone to install something.
// A manifest can be nothing but a name and a dependency list.
// Installing it installs all four.
{
"name": "backend-standard",
"version": "1.0.0",
"description": "Standard plugin set for backend engineers",
"dependencies": [
"secrets-vault",
"deploy-kit",
{ "name": "db-migrate", "version": "^3.0" },
"oncall-runbook"
]
}
# Disabling a dependency something else needs is refused, and the
# error hands you the command that works:
# secrets-vault is still required by deploy-kit. Disable that
# plugin first, or disable everything together:
# claude plugin disable deploy-kit@acme-tools && \
# claude plugin disable secrets-vault@acme-tools
# Auto-installed dependencies outlive the plugins that pulled them.
claude plugin prune --dry-run
claude plugin uninstall deploy-kit --pruneAuto-update is off by default for non-Anthropic marketplaces, so a new bundle version does not arrive on its own. Either enable auto-update for the marketplace, or tell people to run the update and then reload plugins — the reload is what installs the dependencies the new version added. For an organisation, put the bundle in enabledPlugins in managed settings and skip the asking entirely.
By default Claude Code refuses to auto-install a dependency that lives in a different marketplace from the plugin declaring it, which stops one catalogue quietly pulling plugins out of a source you never reviewed. To allow it, the maintainer of the root marketplace — the one hosting the plugin being installed — lists the target marketplace in allowCrossMarketplaceDependenciesOn.
Only the root marketplace's allowlist is consulted, so trust does not chain through intermediate catalogues. Without the field the install fails with a cross-marketplace error naming exactly what to set. A user can still install the dependency by hand first, which satisfies the constraint without anyone editing an allowlist — useful once, and a bad habit as policy.
Claude Code intersects every installed plugin's range and resolves to the highest version satisfying all of them. Two properties follow that are worth stating plainly: auto-update keeps updating a constrained dependency within its allowed range rather than freezing it, and uninstalling the last plugin that constrained something releases the hold on the next update.
How common combinations resolve:
| Constraints in play | What happens |
|---|---|
| Caret 2.0 and at-least 2.1 | One install at the highest 2.x tag at or above 2.1.0. Both plugins load |
| Tilde 2.1 and tilde 3.0 | The second install fails with range-conflict. The first plugin and the dependency are left exactly as they were |
| An exact pin, nothing else | The dependency stays at that version and auto-update skips newer ones while the pinning plugin is installed |
| No tag satisfies all ranges | Auto-update skips that dependency and lists the skip in the plugin Errors tab, naming the constraining plugin |
The lifecycle rules are strict in a way that is easy to live with once you know them:
Two habits keep this tidy. Run the prune command with its dry-run flag occasionally to see which dependencies no longer have anyone requiring them — plugins you installed yourself are never touched, only ones that arrived through a dependencies array. And when you know a plugin is going for good, uninstall it with the prune flag so the cleanup happens in the same step.
Constrain the dependencies you actually rely on rather than all of them, because every range you write is a range someone eventually has to widen. Tag your releases with the command rather than by hand, since it checks that the manifest and the marketplace entry agree before it writes anything. And if you maintain a plugin other teams depend on, publish tagged releases even when nobody has asked yet — an untagged upstream makes every downstream constraint unenforceable.
Sources & further reading