Skip to content

Instantly share code, notes, and snippets.

@woods
Created May 26, 2026 14:29
Show Gist options
  • Select an option

  • Save woods/8d80a93575bf209bc39c11223ed58745 to your computer and use it in GitHub Desktop.

Select an option

Save woods/8d80a93575bf209bc39c11223ed58745 to your computer and use it in GitHub Desktop.

Claude Code plugin architecture

This document is the mental model behind how a Claude plugin gets installed. Normal plugin instructions tell you what to do; this document tells you why it works, what each piece actually is, and how the parts fit together. Reading it should take maybe ten minutes and save you hours of confusion later when something doesn't behave the way you'd expect from reading a plugin's surface docs alone.

The three-layer model

The single most useful idea for understanding Claude Code plugins is that the system has three layers, and they're independent.

The first is declaration — what the project says should be installed. It lives in this repo's .claude/settings.json, committed alongside the code. A collaborator who clones the repo gets the declaration as part of the source tree.

The second is install — what's actually on disk on your machine. It lives in ~/.claude/, user-scoped, never committed. The declaration says "this plugin should be present"; the install is the act of cloning the plugin's code into your home directory and recording that it's active.

The third is runtime — what's loaded into a given Claude session. It's constructed each time a session starts by reading the user-level install and applying it. The runtime is ephemeral; the install is durable.

This three-layer split is the same idea you already know from package managers: a package.json declares dependencies, npm install fetches them into node_modules, and a running process loads them. The difference is that the install is user-scoped rather than project-scoped — there's no per-project node_modules equivalent. Every plugin you install lands in one shared ~/.claude/plugins/ directory and applies to every Claude Code session on that machine.

The practical consequence: a new collaborator clones the repo and gets the declaration but not the install. The first Claude Code session they open in the repo notices the discrepancy and prompts them to install the declared plugin. Once they accept, the plugin lives at the user level on their machine and every Claude Code session on that machine — CLI, desktop app, Zed adapter — sees it from then on.

What's actually in a plugin

A Claude Code plugin is a directory with a .claude-plugin/plugin.json manifest. The manifest declares a name, version, and what the plugin contributes. The pieces a plugin can ship are skills, hooks, slash commands, and MCP servers.

Skills are markdown files with YAML frontmatter that describe a workflow or pattern. Each skill has a name and a description written in trigger language ("Use when..."). The agent reads the available skills' descriptions and decides — heuristically, based on your message — whether to invoke one. Skills are the most common plugin output. Superpowers, for example, is mostly a skills library: brainstorming, writing-plans, test-driven-development, systematic-debugging, and so on.

Hooks are scripts that fire on lifecycle events. session-start is the most useful: it runs when a Claude session begins, lets the plugin announce itself, register state, or prepare context. Hooks run with your real shell privileges and can be written in any language. They're how a plugin makes itself feel present without depending on the model to discover it.

Slash commands are user-typed shortcuts that expand to predefined prompts or actions. Plugins can register them under the / namespace.

MCP servers are tools that extend what the agent can call — connectors to external systems, custom file readers, database query interfaces. A plugin can bundle one or more MCP servers and they get started on session launch.

Most plugins ship some combination of these. Superpowers is heavy on skills, has a session-start hook for bootstrap, and (as of v5.1.0) does not ship MCP servers.

Marketplaces and how plugins get found

Plugins don't have a central registry like npmjs.com. They're discovered through marketplaces, which are themselves just JSON files (.claude-plugin/marketplace.json) hosted somewhere — typically a GitHub repo — that list one or more plugins and where to fetch them.

When Claude Code reads an extraKnownMarketplaces entry from your project's settings.json, it clones the referenced GitHub repo, reads its marketplace.json, and learns about the plugins available there. Each plugin entry in the marketplace points to a source, often the same repo, sometimes a subdirectory or a different repo entirely.

The Superpowers repo is structured so the marketplace.json and the plugin code live in the same place. That's not required — a marketplace can index plugins from anywhere — but it's the simplest pattern.

Anthropic also publishes a curated catalog of plugins of their own. That's why you'll see two Superpowers entries in the desktop app's Customize → Plugins panel: one under the Code tab from Jesse Vincent's upstream (the superpowers-dev marketplace), and one under Anthropic & Partners from Anthropic's curated catalog. They're separate distributions of overlapping content. This repo uses Jesse's upstream because we can version-pin it directly at a GitHub commit.

Version pinning, and the lockfile that doesn't exist

Here's the part that surprised me coming from package-manager-land: Claude Code has no lockfile. The enabledPlugins declaration in settings.json is just a boolean — it says "install this" but not "install this exact version." Without further intervention, two developers installing at different times will resolve to whatever was at the marketplace's default branch HEAD when each ran the install. That's not acceptable for a repo where you want every collaborator running the same plugin code.

The lever you have is the ref field on the marketplace source. It accepts a branch, tag, or commit SHA and pins the marketplace fetch. Since Superpowers' marketplace.json and plugin code live in the same repo, pinning the marketplace effectively pins the plugin. Our .claude/settings.json uses "ref": "v5.1.0" to lock everyone to that release tag.

Tags are mutable in git — a determined upstream could re-point one — so for maximum determinism you can replace the tag with the underlying commit SHA (find it with git ls-remote https://github.com/obra/superpowers v5.1.0). For this repo, the tag is currently sufficient; if the threat model shifts, tightening to a SHA is a one-line change.

To pick up a newer Superpowers release, you bump the ref value in .claude/settings.json, commit the change, and collaborators see the new version on their next session start. There's no auto-update path, and we wouldn't want one — silent updates to code that registers hooks and runs in your shell would be bad. The friction of a manual bump is a feature.

The harness landscape

You can interact with Claude Code through several front-ends. They all sit on top of the same Claude Code binary and read the same ~/.claude/ state. Understanding this is what makes "install once, available everywhere" work.

The Claude Code CLI is the canonical implementation. Run claude in a terminal from any project directory. Every other harness invokes this binary under the hood. Plugins, hooks, slash commands, settings — all behave exactly as documented.

The Claude desktop app's Code tab is a UI shell around the same Claude Code binary. Same plugins, same skills, same hooks. The slash command surface is not fully exposed in the desktop app's chat input — /plugin marketplace add won't autocomplete or fire there — but graphical equivalents exist in the Customize panel. For plugin installs specifically, the project-level .claude/settings.json plus the install prompt is the path to use; you don't generally need the slash command.

Zed's Claude Agent is Zed's adapter that invokes Claude Code as a subprocess and talks to it via the Agent Client Protocol (ACP). Same binary, same config files, same plugin behavior. You don't install plugins "in Zed" — you install them once into ~/.claude/, and Zed's Claude Agent picks them up the next time you open a thread.

The Claude desktop app's Cowork tab is a different surface entirely — it runs in a sandboxed Linux container rather than on your real machine, and is aimed at general-purpose document/data workflows rather than development. It's not where dev work on this repo happens. If you find yourself running git or invoking uv in Cowork, you're in the wrong tab.

The "install once, available everywhere" property is the architectural payoff. You don't need a separate install ritual for each harness. The plugin install is user-scoped; the harness is just a window onto it.

What "session start" actually means

When you open a Claude Code session — whether by running claude in a terminal, clicking into the desktop app's Code tab, or starting a Zed Claude Agent thread — the following happens, roughly in order. Claude Code reads ~/.claude/settings.json for user-level defaults and enabled plugins. It reads the project's .claude/settings.json if one exists, merging it on top. For each enabled plugin, it loads the plugin from ~/.claude/plugins/, reads its .claude-plugin/plugin.json, and runs any registered session-start hook. The plugin's skills are surfaced into the model's available-skills list. The plugin's slash commands are registered. The plugin's MCP servers (if any) are started and their tools become callable. Only then does the model begin its first turn, with all of the above in context.

This sequence is the same regardless of harness. It's also why an install that worked in the desktop app's Code tab "just works" the next time you open the repo in Zed — Zed's adapter is calling into the same code path.

How skill triggering actually works

Skills aren't deterministic gates. The agent sees each skill's description field at session start and decides, turn by turn, whether your current message warrants invoking one. The skill description is the trigger contract — phrasing like "Use when implementing any feature or bugfix, before writing implementation code" tells the agent when to call it.

This means two things in practice.

First, the framing of your message matters. "Let's figure out what to do next" admits design space and triggers brainstorming. "Implement task 6" doesn't, and goes straight to code. Both are correct; they just engage different skills.

Second, CLAUDE.md is for soft invitations, not hard rules. A CLAUDE.md instruction saying "always use brainstorming before any code change" doesn't override skill triggering — it nudges, but the agent still decides. For things you want guaranteed (test coverage, formatter compliance, commit conventions), the right enforcement layer is CI, pre-commit hooks, or repository code review, not a CLAUDE.md instruction. Skills are practice prompts. They're not control systems.

Trust and the install prompt

Plugins fetch code from a GitHub repo and run hooks in your shell. That's nontrivial trust. The system handles it by making the install an explicit consent moment: when Claude Code sees a declared-but-not-installed plugin (either from /plugin install or from a project's .claude/settings.json), it prompts you to approve the install before fetching anything. Once installed, subsequent session starts use the cached install without re-prompting. A version bump (changing the ref) re-triggers the prompt because the new code hasn't been seen yet.

The implication is real: a hostile commit to this repo's .claude/settings.json cannot silently install a malicious plugin on a collaborator's machine. They'd see the prompt and have a chance to read the diff. Treat the prompt as a code-review moment, not a click-through.

If you want stronger isolation — never trust upstream's tag movements even with the install prompt — fork obra/superpowers to your own GitHub org, pin to a SHA in your fork, and have extraKnownMarketplaces point at the fork. Upstream changes then require an explicit pull-and-review-and-bump in your fork before they can reach anyone.

What lives where

Layer Path Purpose Committed?
Project declaration .claude/settings.json Declares marketplaces and enabled plugins for the repo Yes
Project local .claude/settings.local.json Per-machine overrides (allowlists, etc.) No (gitignored)
User install record ~/.claude/settings.json Records installed plugins on this machine No (not in repo)
Plugin code ~/.claude/plugins/ Cloned plugin repositories No (not in repo)
Plugin manifest ~/.claude/plugins/<plugin>/.claude-plugin/plugin.json What the plugin contributes No
Marketplace manifest ~/.claude/plugins/<plugin>/.claude-plugin/marketplace.json What plugins a marketplace exposes No

Verifying the install worked

The simplest check is to start a Claude Code session in the repo (via the CLI, the desktop app's Code tab, or Zed's Claude Agent) and ask an open-ended planning question — "What should we work on next from IMPROVEMENTS.md?" If Superpowers loaded, the agent will invoke a skill like brainstorming before responding. If you get a direct answer without any skill invocation, something is off and Superpowers' skills aren't reaching the model.

For a more definitive check, ask the agent directly: "List the skills currently available to you." It should enumerate the Superpowers ones (brainstorming, writing-plans, test-driven-development, systematic-debugging, and the others) alongside any others. Absence of those tells you the install didn't take.

When things go wrong

A few common failure modes, in rough order of likelihood.

The install prompt didn't appear when you opened the repo and the plugin doesn't show as installed in Customize → Plugins. Quit and relaunch the desktop app (not just a new chat session) so it re-reads project settings. If that still doesn't trigger the prompt, install manually via the Customize panel — pick "Superpowers" by Jesse Vincent under the Code marketplace tab, not the Anthropic-curated version under Anthropic & Partners.

The plugin is installed but skills aren't surfacing. Check that the marketplace and plugin names in .claude/settings.json match what's actually published. The plugin name is superpowers; the marketplace name is superpowers-dev. Mismatches fail silently — no error appears, the install just doesn't happen.

Hooks aren't firing. Verify the install at the user level — ~/.claude/plugins/ should contain a superpowers-dev directory with a hooks/ subdirectory and a session-start script. If it's missing, the install didn't complete; reinstall via the Customize panel.

Skills surface but the agent doesn't invoke them. This is usually the framing of the message, not a config problem. Try asking an open-ended question that admits design space. If it still doesn't trigger, check that CLAUDE.md isn't actively contradicting the skill — instructions like "be terse, just answer" can suppress skill invocation.

Further reading

The authoritative Claude Code documentation lives at code.claude.com. The pages below are the ones worth reading directly when this doc's mental model isn't enough.

For the plugin system itself, in roughly the order you'd want to encounter them: Plugins overview introduces what a plugin is and how to author one. Plugin marketplaces explains the marketplace layer — how to create one, how plugins are listed, and how the ref field controls version resolution. Discover and install plugins covers the install workflow from the user's perspective, including the auto-prompt behavior when a project declares a plugin that isn't yet installed. The plugins reference is the schema reference — open it when you need to know what fields are valid in plugin.json, marketplace.json, or hooks.json.

For configuration: the settings reference documents the full settings.json schema, including extraKnownMarketplaces, enabledPlugins, permissions, and the relationship between user-level and project-level files. Skills covers how skills are structured and surfaced to the model — useful background even if you're consuming skills rather than authoring them.

For the harness layer: Desktop application covers the Claude desktop app's Code tab and where it differs from the standalone CLI. External agents in Zed describes Zed's overall integration with Claude Code and other agents. Claude Code in Zed via ACP is the technical writeup of how Zed's Claude Agent invokes Claude Code via the Agent Client Protocol — read this if you want to understand why an install at ~/.claude/ automatically propagates to Zed.

For the plugin this repo actually uses: obra/superpowers is Jesse Vincent's upstream repository. The README documents the individual skills, the plugin's design philosophy, and the hook bootstrap mechanism. The repo's RELEASE-NOTES.md is the source for "what changed in v5.x.y" when you're deciding whether to bump the pinned ref.

For the protocol that underlies MCP servers (one of the four plugin contribution types): Model Context Protocol is the protocol spec. Worth reading if you ever author or evaluate a plugin that ships an MCP server, since the protocol determines what kinds of tool integrations are possible.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment