I built content-stack (opens in a new tab) — a Claude Code plugin marketplace with two plugins I use on my own projects. It worked well locally. The setup felt clean: register the marketplace in .claude/settings.json, list the plugins under enabledPlugins, commit it, done.
Then I tried it in a cloud session — Claude Desktop running in a remote, headless environment.
The plugins weren't there. No error in the terminal. No warning in the session. Just nothing.
What the configuration looks like
The setup that should work:
{
"extraKnownMarketplaces": {
"content-stack": {
"source": {
"source": "github",
"repo": "pcamarajr/content-stack"
}
}
},
"enabledPlugins": {
"content-ops@content-stack": true,
"cost-tracker@content-stack": true
}
}
This is exactly what the docs describe. The marketplace is registered, the plugins are enabled. Locally, it works. In a cloud or headless session, the plugins silently don't install.
What's actually happening
The root cause isn't a bug — it's an architectural constraint. Claude Code's plugin auto-installation is tied to the interactive trust dialog. When you open a project interactively, Claude Code prompts you to trust the folder. That trust event is what triggers marketplace registration and plugin installation.
In cloud sessions — claude.ai, headless CI, Docker containers — the trust dialog is skipped entirely. No dialog means no trust event. No trust event means plugins never install. The enabledPlugins setting is just ignored, silently.
It's one of those failures that's particularly hard to debug because there's no feedback. You're not doing anything wrong. The setting is correct. Claude Code just has no hook to act on it.
The fix: a bootstrap script
In Claude Desktop, environments (opens in a new tab) define where and how a cloud session runs. Each environment has a Setup script field — a Bash script that runs automatically when a new session starts, before Claude Code launches. That's exactly the hook we need.
To set it up: open the environment dropdown in Claude Desktop, create a new environment (or edit an existing one), and paste the script into the Setup script field.
#!/bin/bash
# bootstrap-plugins.sh
set -e
claude plugin marketplace list | grep -q "content-stack" || \
claude plugin marketplace add pcamarajr/content-stack
claude plugin list --installed | grep -q "content-ops" || \
claude plugin install content-ops@content-stack
claude plugin list --installed | grep -q "cost-tracker" || \
claude plugin install cost-tracker@content-stack
echo "Plugins ready."
The setup script runs as root on Ubuntu 24.04, has network access to common registries by default, and only fires on new sessions — not resumed ones. The grep -q checks make it idempotent, so re-running won't break anything if the plugins are already installed.
One thing worth noting: this is different from a SessionStart hook (opens in a new tab), which runs after Claude Code launches. The setup script runs before, which is what makes it work here — by the time Claude Code starts, the plugins are already in place.
For Docker or CI environments there's an official alternative: the CLAUDE_CODE_PLUGIN_SEED_DIR environment variable. Point it to a pre-populated ~/.claude/plugins/ directory built into the image. No runtime installation, no script needed — but it requires building the seed directory at image creation time.
This is a known issue
I wasn't the first to hit this. There are multiple open issues in anthropics/claude-code (opens in a new tab) documenting the same problem:
- #12840 (opens in a new tab) — Headless/programmatic plugin installation (closed: not planned)
- #13096 (opens in a new tab) — Support
extraKnownMarketplacesin headless/print mode (closed: not planned) - #13097 (opens in a new tab) — Docs should clarify that
extraKnownMarketplacesrequires interactive trust dialog (closed: stale) - #19522 (opens in a new tab) — Non-interactive plugin installation (closed: not planned)
- #23737 (opens in a new tab) —
autoInstallEnabledPluginssetting proposal (closed: duplicate) - #32607 (opens in a new tab) — No warning when
enabledPluginsreferences an uninstalled plugin (closed: duplicate)
The pattern across all of them: Anthropic has consistently closed these without shipping a declarative solution, while quietly shipping CLAUDE_CODE_PLUGIN_SEED_DIR as the intended path for container use cases.
Issue #32607 is the one worth watching — or upvoting. At minimum, Claude Code should warn when enabledPlugins references plugins that aren't installed. A single log line would have saved me the entire debugging session.
What to take from this
If you're building or using Claude Code plugins and expect them to work in cloud or CI environments, you need an explicit installation step. The enabledPlugins setting alone isn't enough.
The bootstrap script is simple and works today. If you're working with Docker or CI pipelines, CLAUDE_CODE_PLUGIN_SEED_DIR is the cleaner long-term approach. Either way, it's a one-time fix once you know why.
The harder problem is that nothing tells you this. You commit a correct-looking configuration, start a cloud session, and get no feedback on why the plugins didn't load. Until there's a warning for that — and there should be — now you know.