Field notes
2026-05-22 — Plugin loader chain (install → SDK → MCP)
Onboarding the first Claude Code marketplace plugin (excalidraw-diagram
from openskill) surfaced four bugs in the install → SDK → MCP pipeline.
Each fix unblocked the next failure mode. Documenting the chain so the
next plugin we onboard works end-to-end without surprises.
The four landmines#
1. EXDEV staging (v0.8.7)#
bun run install-deps cloned each plugin/skill/KB into
$SLAUDE_HOME/.tmp and then renameSync'd the clone into its final
location. On bare boxes this is fine — $SLAUDE_HOME is one filesystem.
In the k8s deploy we subPath-mount $SLAUDE_HOME/{.claude,skills,knowledge}
as separate PVC volumes, so .tmp/ ends up on the container's overlay fs
and renames cross device boundaries → EXDEV: cross-device link not permitted.
Fix: stage inside a .tmp/ sibling of each destination subPath
(paths.claudeConfig/plugins/.tmp, paths.skills/.tmp, paths.knowledge/.tmp).
2. SDK ignores enabledPlugins (v0.8.8)#
After install-deps lands the plugin under plugins/cache/<mp>/<plugin>/
and writes the CC plugin metadata (installed_plugins.json,
known_marketplaces.json, settings.json.enabledPlugins), the running
SDK still doesn't expose the plugin's skills or MCPs. claude-agent-sdk
only reads those files if you opt in via Options.settingSources: ['user'] — which would also pull operator CLAUDE.md and unrelated
settings.
Fix: read installed_plugins.json ourselves, translate each entry into
{ type: 'local', path }, and pass via Options.plugins. See
src/config/plugins.ts:loadInstalledPluginPaths.
3. --plugin-dir skips .mcp.json (v0.8.9)#
Options.plugins serialises to --plugin-dir <path> on the child CLI.
The CLI's cg5() (session plugin loader) loads
skills/commands/hooks/agents/output-styles from each path — but does NOT
mount the plugin's .mcp.json MCP servers. Verified against the
literal cli.js@2.0.77 shipped with the SDK: only dg5() (the path for
plugins enabled via settings) walks .mcp.json, and we deliberately
avoid that path.
Fix: read each installed plugin's .mcp.json ourselves and merge into
Options.mcpServers. See loadInstalledPluginMcps.
4. Bun image has no npx (v0.8.10)#
excalidraw-diagram/.mcp.json launches its server via
{ "command": "npx", "args": ["drawmode", "--stdio"] } — the generic
Claude Code convention. The slaude base image is oven/bun:1.3-debian,
which ships bunx (and a node shim) but no npx. The stdio MCP
client fails to spawn, silently, and the tool just never appears.
Fix: transparently rewrite command: "npx" → command: "bunx" when
ingesting plugin MCPs (shimStdioCommand in src/config/plugins.ts).
bunx is API-compatible for the "download an npm package and run its
bin" case we care about.
Net result#
slaude.json declares one entry:
{ "plugins": [{ "marketplace": "git@bitbucket.org:org/openskill.git",
"plugin": "excalidraw-diagram", "ref": "main" }] }On boot: install-deps clones the marketplace + plugin, writes CC
plugin metadata, and the agent session shows up with the
excalidraw-diagram skill + the excalidraw MCP server fully
connected (verified in staging maria).
Onboarding a new plugin#
Add it to slaude.json plugins, restart the pod, done. If the plugin's
.mcp.json uses anything other than npx and we don't have the
toolchain in the base image (uv? deno?), either:
- install the toolchain in the deploy Dockerfile (we already do
this for
uvx/mcp-grafana), or - add another shim entry in
shimStdioCommand.