Agents: per-instance Claude Code plugin enablement (bake once, enable per instance) #72
Labels
No labels
area:agents
area:dashboard
area:database
area:design
area:design-review
area:flows
area:infra
area:meta
area:security
area:sessions
area:webhook
area:workdir
security
type:bug
type:chore
type:meta
type:user-story
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
charles/claude-hooks#72
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
User story
As the operator, I want to pick which Claude Code plugins a given
agent instance runs with — independent of its type — so that a
devinstance touching the Python MCP can have Pyright LSPenabled while another
devinstance touching only TypeScript hasTypeScript LSP enabled, and a
designerinstance working onbrutalist mockups can have Frontend Design without forcing it on
every designer dispatch.
This is the next axis on the milestone-16 per-instance customization
surface, next to model override (#48), prompt appendix (#51),
match-labels (#50), and the dashboard CRUD that surfaces them
(#53).
Design principle: separate install from enable
Install is image-side. The container image bakes in every
vetted Anthropic-verified plugin we might want — one-time build
cost, cheap storage (~100 MB per LSP, skills are a few KB each).
Enable is instance-side. Each agent instance's isolated
CLAUDE_CONFIG_DIRcarries asettings.jsonthat lists only theplugins that instance should activate. The SDK's Claude CLI ignores
installed-but-not-enabled plugins, so unused ones cost nothing at
runtime.
This means:
no rebuild.
breaking other instances.
Current state (2026-04-19)
boss,dev,reviewer,designer,design-reviewer— each with afixed row in
config/agents.json.image; the fork-maintained
forgejo-mcp+penpot-mcpare bakedin but the wider Claude Code plugin ecosystem is untouched.
for this axis; a
pluginscolumn / join fits cleanly.Acceptance criteria
Image — bake in the approved plugin set (one-time)
Dockerfileinstalls the vetted plugins during build viaclaude plugin install <id>steps, idempotent and offline-capable. Each plugin pins a version.
claude.com/plugins, all Anthropic-
verified):
-
frontend-design-
security-guidance-
claude-md-management-
typescript-lsp-
pyright-lsp-
pr-review-toolkit-
skill-creator-
hookify-
plugin-developer-toolkit-
mcp-server-dev-
agent-sdk-devscripts/smoke-creds.shgrows a plugin-presence probe:docker exec claude-hooks-<agent> claude plugin listshouldinclude every approved plugin's id. Regression gate if an image
build drops a plugin.
Config — per-type defaults + per-instance overrides
config/agents.jsongrows aplugins: [<id>, …]key pertype. Acts as the default enable-set new instances of that
type inherit.
-
boss/dev:security-guidance,typescript-lsp, pluspyright-lspif the instance is labelled to touch Python.-
reviewer:security-guidance,typescript-lsp,pr-review-toolkit.-
designer:frontend-design,security-guidance.-
design-reviewer:frontend-design,security-guidance.pluginscolumn — JSON array of plugin ids. NULL = inherit type default.
A non-null value fully overrides (not layered) so the
edit-semantics stay predictable in the UI.
Runtime — per-instance
CLAUDE_CONFIG_DIRcarries the enable-setper-instance
settings.jsoninto that instance's isolatedCLAUDE_CONFIG_DIRwith the enabled plugin list beforespawning the SDK
query().seedContainerClaudeJsonfrom PR #67 / issue #56). Addingsettings.jsonalongside the existing.claude.json+.credentials.jsonseed is a one-line extension ofseedContainerClaudeJson— same idempotent pattern.instance's plugin list — just the next task dispatch reads
the fresh
settings.json.Dashboard (hooks into #53)
checkbox per plugin, defaulted to the type's default set.
Hooks / Slash commands) as a chip next to the name so the
operator knows the cost profile at a glance.
default (UX: "override" badge).
Tests
buildMcpSetupequivalent for plugins — given aninstance config, assert the emitted
settings.jsonhas theright
enabledPluginsfield and no stray enable flags.plugins: nullin SQLite resolves to the type default; instance with an
explicit empty array
[]resolves to no plugins (escapehatch for debugging).
frontend-designdisabled gets the abort-diagnostic if its skill tries to use
a Frontend Design tool. A designer instance with it enabled
resolves the tool.
Documentation
CLAUDE.md§ Plugin management section: how to propose anew plugin (add to Dockerfile + approve via PR), how to enable
it per instance (dashboard).
README.md: one-liner on the per-instance plugin axis.penpot-mcp-server/CHANGELOG.mdisn't affected — this ticketis service-side only.
Out of scope
plugins land in the baked set. A separate story if we ever want
community plugins — the trust model is different.
per-dispatch. A task can't opt in to a plugin the instance doesn't
have. Keeps the surface clean.
set; bumping a plugin version = Dockerfile edit + image rebuild,
not dashboard work. Version pins live in
Dockerfile+CHANGELOG.auto-discovery, PR Review Toolkit's confidence threshold). Each
plugin's own settings stay inside its manifest; this ticket only
flips the on/off bit.
References
pluginscolumn).its plugin set together determine "what agent picks up this issue
with what toolkit").
seedContainerClaudeJsonis the extension point wherethe per-instance
settings.jsongets written.CLAUDE_CONFIG_DIRmeans plugin enable-setsare already strongly partitioned — the interactive user's config
can't leak.
Dependencies
lands all three axes' dashboard edit surfaces in one pass.
main, after #48 lands.Suggested breakdown
smoke-creds.shwith a presence probe. PR includes the pluginlist as a constant somewhere the service can read.
config/agents.json— addpluginskeyper type, document the semantics.
seedContainerClaudeJsonextension — also seedsettings.jsonwith the resolved enable-set per task.pluginscolumn (on top of #48) + resolver:instance → type → default.