Shipping A Plugin
Packaging skills, agents, commands and definitions as a distributable Claude Code plugin.
A Claude Code plugin is a directory of Markdown files with YAML frontmatter, plus a JSON manifest.
Writing them by hand works right up until two agents need the same paragraph of instructions, at which point you are maintaining copies. You know the rest of that story — it is the first page of this section.
psy init my-plugin --template claude-plugin
cd my-plugin
psy check
psy buildThe layout
my-plugin/
psy.config.ts
src/
framework.psy your local vocabulary, if you want one
skills/
agents/
commands/
concepts/ what the plugin's users are building
references/
index.psy
plugin/ generated
.claude-plugin/plugin.json
agents/*.md
skills/*/SKILL.md
commands/*.mdThe only difference from a project build
One option.
import { defineConfig } from "@psy/config";
import { morphic } from "@psy/morphic";
import { claude } from "@psy/adapter-claude";
export default defineConfig({
source: ["./src"],
frameworks: [morphic()],
targets: [
claude({
output: "plugin",
plugin: {
name: "my-plugin",
description: "Agents, skills and commands defined in Psy.",
version: "0.1.0",
license: "MIT",
},
}),
],
});Omit plugin and you get a .claude/ directory for one project. Include it and
you get the plugin layout plus the manifest:
{
"name": "my-plugin",
"description": "Agents, skills and commands defined in Psy.",
"version": "0.1.0",
"license": "MIT"
}Everything else — how a skill renders, what becomes a section, which frontmatter
field when lands in — is identical to
What Gets Generated.
What you get
psy buildplugin/.claude-plugin/plugin.json
plugin/agents/build-feature.md
plugin/agents/build-project.md
plugin/commands/review.md
plugin/skills/implement-change/SKILL.md
plugin/skills/project-context/SKILL.md
plugin/psy-generated.mdA generated command, in full:
---
description: Review the current change.
argument-hint: "[path]"
allowed-tools: "Read, Grep, Bash(git diff:*)"
---
<!-- Generated by Psy from src/commands/Review.psy. Do not edit by hand; edit the .psy source and run `psy build`. -->
Review the change at the given path, or the current diff when no path is
given.
## Skills
- `project-context` (ProjectContext)Ship the definitions too
This is the part that is easy to miss, and it is most of the value of building a plugin in Psy rather than by hand.
A plugin is usually for something — a framework, a platform, a house style. Its skills are only as good as the reader's understanding of that thing, and you cannot assume the reader has one.
So put it in the plugin:
export concept OurStack:
description: How applications on our platform are put together.
overview:
Every service is a single deployable that owns its data. Services talk
over the event bus, never by calling each other directly.
export reference ErrorStyle:
description: How errors are surfaced to users.
guidance:
Say what happened, then what to do about it. Never show a stack trace
to an end user.export skill ImplementService:
description: Implement a change in a platform service.
when: Use when changing anything inside a service.
context:
- OurStack
- ErrorStyle
prompt:
Keep cross-service calls on the event bus.---
name: our-stack
description: "Concept: How applications on our platform are put together."
---
# OurStack
## Overview
Every service is a single deployable that owns its data. Services talk over the
event bus, never by calling each other directly.
## Authority
This describes the product as it is meant to work. Where the implementation
disagrees, treat this as correct unless the task is explicitly to change it.---
name: implement-service
description: Implement a change in a platform service. Use when changing anything inside a service.
---
# ImplementService
Keep cross-service calls on the event bus.
## Context
Consult these definitions of how the product works:
- `our-stack` (OurStack)
- `error-style` (ErrorStyle)Those definitions ship as skills alongside the working ones, and get loaded when relevant. Anyone installing your plugin gets the instructions and the model of the thing the instructions are about.
Specifying Software is the whole chapter on writing these well.
Scoping one build from many
If a repository produces more than one plugin, or a plugin plus a project-local
.claude/, scope each target with modules:
targets: [
claude({
output: "plugin",
modules: ["src/plugin/"],
plugin: { name: "my-plugin", version: "0.1.0" },
}),
claude({
output: ".claude",
modules: ["src/internal/"],
}),
],Without modules, every target emits every declaration in the program, and you
will ship your internal agents to strangers.
Keeping it honest
psy build --checkfails when any generated file is missing, out of date, or no longer produced. Wire
it into CI next to your tests. Generation is deterministic, so a clean --check
means the committed plugin matches the Psy sources exactly.
Renaming a declaration removes its old artifact, provided the build can prove it
wrote the file — see pruning. For a plugin you commit and
publish, turn artifacts.manifest on; a stale skill left behind after a rename is
a file your users will load.
Names
A spec's PascalCase name becomes a kebab-case artifact name:
ReadPsySpec -> read-psy-spec
BuildCLI -> build-cli
IdentifySpecGaps -> identify-spec-gapsTwo declarations that would collide on the same slug are a framework error, not a silently overwritten file — and the check spans skills, agents, commands and all five definition kinds at once.
This is how Psy maintains itself
The same mechanism produces the agents that maintain the Psy compiler. build/ in
the Psy repository declares BuildPsy, ReviewPsy, DocumentPsy and the
specialised build agents, and psy build writes them into .claude/.
See Psy Describes Psy.
Next: Tutorial: A Paint App