Plugin dependencies
Declare the plugins your plugin relies on, pin them with semver ranges, tag releases so ranges resolve, and understand how conflicts play out for users.
Sooner or later one plugin needs another. Your release-kit calls an MCP tool provided by audit-trail; your team's starter bundle is really just a list of other plugins. Plugin dependencies let you declare that relationship so Claude Code installs, checks and prunes the pieces for you.
This page is for authors writing dependencies in plugin.json, and for maintainers whose plugin others depend on. Users installing a plugin with dependencies should read /docs/plugins/install. npm or Bun packages your own code needs are a different mechanism, covered in /docs/plugins/loading.
Why pin a version
An unconstrained dependency follows its marketplace. Every time users update, they get whatever the dependency author released last. If that release renames the MCP tool you call, your plugin breaks for every user overnight, and you did not change a line.
A constraint such as ~1.4.0 on a git-backed dependency keeps users on 1.4.x patches. When 1.5 comes out, you test against it, widen the range, and ship a new version of your own plugin on your schedule. I pin anything whose tool names or skill names my plugin relies on.
Declaring dependencies
Add a dependencies array to .claude-plugin/plugin.json:
{
"name": "release-kit",
"version": "2.0.0",
"dependencies": [
"changelog-tools",
"shared-hooks@platform-plugins",
{ "name": "audit-trail", "version": "~1.4.0" }
]
}
Entries come in two shapes.
String. Either a bare plugin name ("changelog-tools"), looked up in the same marketplace as your plugin, or "name@marketplace" to look in another marketplace. A string means "whatever version the marketplace currently offers".
Object. Use this to add a constraint or a different marketplace:
| Field | Required | Meaning |
|---|---|---|
name | Yes | The dependency's plugin name as listed in its marketplace |
version | No | A semver range: ~1.4.0, ^2.0, >=1.4, =2.1.0 and so on. Installs the highest git tag that satisfies it, so the dependency must have tagged releases |
marketplace | No | A different marketplace to resolve name in. Subject to an allowlist (below) |
Pre-releases such as 2.0.0-beta.1 do not match a normal range. Opt in with a pre-release suffix, for example ^2.0.0-0.
A bundle plugin for a team
Because a manifest only needs name, a plugin can consist of nothing but dependencies. That makes a neat "starter pack":
{
"name": "frontend-starter",
"version": "1.2.0",
"description": "Everything a new frontend engineer at Acme needs",
"dependencies": [
"design-tokens",
"a11y-review",
{ "name": "storybook-helper", "version": "^4.0" },
"pr-hygiene"
]
}
New starters run one claude plugin install frontend-starter@acme-plugins instead of four. To add a fifth plugin later, publish frontend-starter 1.3.0 with an extra entry. If the marketplace does not auto-update by default, engineers either switch auto-update on for it, or run claude plugin update frontend-starter and then /reload-plugins in any open session to pull in the new dependency.
To push a bundle to the whole organisation, an admin adds it to enabledPlugins in managed settings; see /docs/plugins/org.
Depending across marketplaces
By default Claude Code will not install a dependency from a different marketplace than the plugin declaring it, unless the user already has that dependency installed and enabled at the same scope. This stops one catalogue from quietly pulling software from a source the user never reviewed.
To permit it, the root marketplace (the one hosting the plugin being installed) lists allowed targets in allowCrossMarketplaceDependenciesOn in its marketplace.json. Only the root's allowlist counts.
{
"name": "acme-plugins",
"owner": { "name": "Acme Platform Team" },
"allowCrossMarketplaceDependenciesOn": ["acme-shared"],
"plugins": [
{
"name": "release-kit",
"source": "./release-kit",
"dependencies": [{ "name": "audit-trail", "marketplace": "acme-shared" }]
}
]
}
What happens without the allowlist entry depends on where the dependency is declared:
- In the marketplace entry: the install is refused with a message beginning
Dependency "audit-trail@acme-shared" (required by release-kit@acme-plugins) is in marketplace "acme-shared", which is not in the allowlist, naming the field to set. - In
plugin.json: the install completes without the dependency, and your plugin then fails to load.
If the user installed and enabled audit-trail from acme-shared themselves first, at the same scope, no allowlist is needed.
Developing two plugins together
When you are working on both your plugin and its dependency, load both from disk:
claude --plugin-dir ./audit-trail --plugin-dir ./release-kit
The local copy satisfies the dependency, so nothing is fetched from a marketplace.
- Version constraints are not checked against local copies, so the dependency's
plugin.jsondoes not need aversion. - From v2.1.242 an entry naming a marketplace (
name@marketplaceor themarketplacefield) also matches the local copy. - If both live in one parent folder that is not itself a plugin,
--plugin-dir ./pluginsonce loads every child with.claude-plugin/plugin.json(v2.1.265+).
Until the dependency is properly installed, your plugin depends on that local copy:
- Local copy disabled: your plugin is disabled at the next load with an error saying the dependency is disabled and telling you to enable it or remove the dependency. If it names the dependency as
<name>@inline, that refers to the--plugin-dircopy. - Flag forgotten: the error says the dependency is not installed. Add the flag back or install it from the marketplace.
Releasing a plugin others depend on
Constraints resolve against git tags on whichever repository hosts the plugin according to its plugin source:
| Plugin source | Who tags | Which repo |
|---|---|---|
github, url or git-subdir | The plugin author | The plugin's own repository |
Relative path like ./plugins/audit-trail | The marketplace maintainer | The marketplace repository |
Tag format
Tags are <plugin-name>--v<version>, with <version> matching version in that commit's plugin.json. The name prefix lets one repository carry several plugins with separate histories, for example audit-trail--v1.4.2 and release-kit--v2.0.0 side by side.
The easy way is claude plugin tag, run from the plugin folder with an origin remote:
cd plugins/audit-trail
claude plugin tag --dry-run # show the plan
claude plugin tag --push # create and push
Before tagging it:
- validates the plugin,
- checks
plugin.jsonand the marketplace entry agree on version (when inside a marketplace checkout), - requires a clean working tree under the plugin folder,
- refuses if the tag already exists.
Success prints Created tag audit-trail--v1.4.2, then Pushed to origin with --push; without it you get the git push command to run. Other flags are in /docs/plugins/cli-reference. Plain git tag audit-trail--v1.4.2 also works if you keep both version fields in sync by hand.
Non-git sources
Tag resolution only applies to git-backed sources. For npm, archive and command sources, the range does not choose what is fetched, but it is still checked at load, and the dependent plugin is disabled if the installed version falls outside it. The version checked is the dependency's own plugin.json version, so make sure it sets one: a manifest with no version satisfies no constraint.
Claude Code never installs a command-source dependency itself, and never runs a dependency's headersHelper. Users must install such dependencies before your plugin. Those limits also apply to the other operations that install missing dependencies: /reload-plugins, auto-update of the dependent plugin's marketplace, re-running claude plugin install on the dependent plugin, and claude plugin marketplace add.
What users experience
Resolving against tags
For { "name": "audit-trail", "version": "~1.4.0" }, Claude Code installs the highest audit-trail--v tag in range. If none matches:
- Plugin in its own repo: install fails with a message containing
Dependency "audit-trail@acme-plugins" has no git tag satisfying. - Plugin at a relative path: the marketplace's current copy is installed and the range is checked at load. Out of range, the dependent plugin stays disabled and
claude plugin listshows something likeRequires "audit-trail@acme-plugins" ~1.4.0, installed 2.0.0.
From v2.1.196, a marketplace added as a local folder that is a git repository also resolves relative-path constraints against that folder's tags. A non-git folder has no tags, so its current contents are used.
Checking what resolved
claude plugin list shows a tag-resolved dependency with a 12-character commit suffix, for example 1.4.2-3fa9c0d1b7e2. Constraint checks use the tag's version, even if plugin.json at that commit lags. If a tag is force-moved, the next install fetches the new commit rather than reusing a stale cache; see /docs/plugins/loading.
When several plugins constrain the same dependency
Claude Code picks the highest version satisfying every range.
| Plugin A | Plugin B | Outcome |
|---|---|---|
^1.0 | >=1.3 | One install at the highest 1.x tag from 1.3.0 up; both load |
~1.4 | ~2.0 | Installing B fails with has conflicting version requirements; A and the dependency are untouched |
=1.4.0 | none | Held at 1.4.0; auto-update skips newer versions while A is installed |
Auto-update fetches a constrained dependency at the highest tag satisfying all installed ranges, not the marketplace's latest. If ranges do not overlap it leaves the dependency alone and the Errors tab names the constraining plugin. If they overlap but no tag fits, it takes the marketplace's current copy and skips the update when that copy is outside any range.
Uninstall the last plugin constraining a dependency and it goes back to tracking its marketplace on the next update. Auto-installed dependencies that nothing needs any more stay until you run claude plugin prune.