Skip to main content

Plugins for Coding Agents: Extend Claude Code and OpenCode

13 Concepts, 80% of Real Use · ~90-min concept read · a focused day for a first real plugin · From an empty folder to a plugin a teammate installs in one command

This is a hands-on course. You will build one real plugin end to end and ship it:

  • A plugin for Claude Code that bundles four kinds of extension behind one install.
  • A skill — knowledge the agent reaches for on its own when the task fits.
  • A subagent — a focused reviewer with its own clean context.
  • Hooks — code that runs every time, to format what the agent writes and block what it must never do.
  • A marketplace entry, so a teammate gets the whole thing with one command — plus the OpenCode version of the guard, so you see the same idea in the other host.

The rule that explains everything else: you're extending an agent you don't own. The host — Claude Code or OpenCode — owns the loop, owns the model, and runs on the user's machine. Your plugin doesn't run the show; it hands the host pieces to load. Everything in this course follows from that one fact, and it produces four non-negotiables — the whole course is these four, built:

  1. Bundle to share. A plugin is the shareable, versioned form of customization. If it's just for you on one repo, you don't need a plugin — you need a .claude/ folder. Reach for a plugin when others should get it too.
  2. Right lever for the job. Four ways to extend an agent: a skill (knowledge it uses by choice), a subagent (work it delegates), an MCP server (tools and data it reaches), a hook (code that runs on its own at fixed moments). Match the job to the lever.
  3. Must-always is a hook, not an instruction. Anything you write in a skill or CLAUDE.md is advice the model may follow or skip. A hook is code that runs every time. If it has to happen — formatting, a safety gate — it's a hook.
  4. A plugin runs in the user's trust. It can execute code on the installer's machine. Build with least privilege, say what it touches, and treat installing one (and shipping one) as a trust decision.

The four invariants of a coding-agent plugin: bundle to share (the versioned, shareable form of customization); the right lever for the job (skill, subagent, MCP server, or hook); must-always behavior belongs in a hook, not an instruction, because hooks are deterministic and instructions are advisory; and a plugin runs in the user's trust because it executes code on their machine. The organizing rule above them: you are extending an agent you don't own.

Read each Concept asking: which invariant is this?

note

Prerequisites. This page assumes three things.

  1. You drive a coding agent. You've done the Agentic Coding Crash Course — Claude Code or OpenCode, plan mode, a rules file. We build through that workbench.
  2. You can read typed code — shell and a little JSON and TypeScript — directly or by pasting blocks to your agent for a plain-English explanation.
  3. Recommended: Connector-Native Apps. That course taught the same habit — know which layer you're on — and built an MCP server. A plugin can bundle one (Concept 6), so the two connect.

You don't need Build AI Agents or AI Identity first. Both come later on the path, and this course points to them.

note

Where this sits. In Mode 2, right after Connector-Native Apps. The path runs: Connector-Native Apps → this course → AI Identity (sign-in & agent access) (coming soon)Build AI Agents. Connector-native apps extend the chat app for end users; plugins extend the coding agent for builders. Same move, the other host.

Setup (a few minutes)

  1. Download the starter (plugins-crash-course-starter.zip), unzip, and cd into the agent-factory folder. That folder is a marketplace; the plugin itself lives in plugins/agent-factory/.
  2. Run ./setup.sh once. It creates two symlinks inside the plugin for single-source sharing: CLAUDE.md → AGENTS.md (so Claude Code and OpenCode read the same house rules), and .opencode/skills → ../skills (so OpenCode discovers the same skills Claude Code uses).
  3. Make sure you have jq (a tiny JSON command-line tool); the hooks use it to read event data.
  4. Prove the starter works: bash plugins/agent-factory/tests/test_hooks.sh — you should see ALL PASS (the guard hook exits 2 on .env and rm -rf, 0 otherwise).

What's in the box. A real, working marketplace with one pluginagent-factory — that shows every lever, built on the cross-tool pattern from Concept 2 (skills port, hooks don't). The repo uses the documented layout: a marketplace at the root, the plugin in a plugins/<name>/ subfolder.

agent-factory/                                  ← the marketplace (repo root)
.claude-plugin/marketplace.json catalog: lists the plugin below
plugins/agent-factory/ ← THE PLUGIN
.claude-plugin/plugin.json the manifest
skills/loop-engineering/SKILL.md a COMPLETE, portable sample skill — read it
skills/review/SKILL.md a STUB you write in the worked example
agents/reviewer.md a STUB subagent you write in the worked example
hooks/format.sh PostToolUse: format on write (complete)
hooks/block-secrets.sh PreToolUse: block .env + destructive commands (exit 2)
hooks/hooks.json wires the hooks
.mcp.json.example remote MCP wiring (4th lever) — point it at your server
.opencode/plugins/guards.ts the SAME guard for OpenCode (hooks don't port)
AGENTS.md house rules (CLAUDE.md is the symlink)
tests/test_hooks.sh proves the guard blocks
server/ ← THE REMOTE MCP SERVER (you deploy this)
index.js a COMPLETE remote server (Streamable HTTP + bearer auth)
test_server.mjs proves auth blocks, and the tool lists + calls
package.json pins @modelcontextprotocol/sdk v1.x

The hooks and the sample skill are complete — read them line by line. The review skill and the reviewer subagent are TODO: you build those in the worked example. The MCP lever is split the way it works in real life: the plugin holds only .mcp.json.example (the pointer), and the server it points at lives in server/ — a complete, runnable remote MCP server you deploy and the plugin reaches by URL (Concept 6).

From here the chapter shows you the shape of each piece; you read it, direct your coding agent to write and run it, and verify what comes back. You direct; the agent types; you check.


Part 1: The shape

Concept 1: You extend the agent — you don't own it

A plugin is not a program you run. It's a set of pieces a host loads and runs for you. The host — Claude Code or OpenCode — owns the agent loop (the decide-act-repeat cycle), brings the model, and runs on the user's machine. Your plugin contributes capabilities and rules the host picks up. Get that picture right and the rest is detail; get it wrong and you'll keep trying to make the plugin "do" things it was never in charge of.

There's a pleasing twist here, and it's worth naming: you direct a coding agent to build a plugin for coding agents. You'll tell Claude Code to write the very kind of extension that Claude Code loads. The thing building it and the thing it extends are the same kind of tool. That's not a gimmick — it's the fastest way to build one, and it's how every Manufacturing course works: you direct, the agent types, you verify.

Concept 2: Two hosts, one idea

This course covers two hosts. They package extensions differently, but the idea is identical: a unit the host loads.

  • Claude Code takes a declarative bundle — a folder of components (skills, subagents, hooks, MCP servers) described by a small manifest. You mostly write configuration and scripts; Claude Code wires them in.
  • OpenCode takes a code module — a JavaScript/TypeScript file that hooks into the agent's events and can add tools. You write functions; OpenCode calls them.
Claude Code pluginOpenCode plugin
Forma folder + plugin.json manifesta .ts/.js module that exports functions
You writeskills, subagents, hooks config, .mcp.jsonevent handlers, custom tools
Loaded froma marketplace, or --plugin-dir.opencode/plugins/ or an npm package

They don't extend better or worse — they extend differently, and the difference is worth knowing before you pick a target. Claude Code is Anthropic's tool, tied to Claude models, with a declarative bundle format and a marketplace ecosystem to distribute through. OpenCode is open-source and model-agnostic — bring your own API key, or run free or local models (Gemini, GPT, local) — and its plugins are code, which buys finer-grained control at the cost of writing more yourself. For cost-sensitive learners that model freedom is the headline: the same skill you write can run on a free model in OpenCode. (The two also interoperate — skills cross over directly, and community plugins bridge credentials and backends between them — but that's using the tools together, not authoring plugins, so it's out of scope here.)

Most of this course is the shared mental model and the Claude Code form (it's the richer bundle); Part 5 shows the OpenCode form for the same job. Pick the host you actually use; the four invariants don't change.

One lever ports for free; one does not. A skill is just a SKILL.md file, and all three coding agents in this course read that format natively — Claude Code, OpenCode, and Codex. OpenCode discovers skills on its own from .opencode/skills/, .claude/skills/, and .agents/skills/ (no plugin needed for skills at all). So one skill folder can serve every tool. Hooks are the opposite: Claude Code hooks are JSON config, OpenCode hooks are a JavaScript module — there's no shared format, so you write a hook once per host. Keep that split in mind; it shapes how you lay out a cross-tool plugin (Concept 4).

That split scales up to two families. The Claude Code plugin you build also loads in Claude Cowork and claude.ai — they share Anthropic's plugin format. The OpenCode plugin you build also loads in OpenWork, an OpenCode-powered desktop agent — same OpenCode format. Skills cross both families (everything reads SKILL.md); the bundles don't cross (a Claude bundle isn't an OpenCode module). So the host you target decides how far the bundle travels — but a plain skill travels everywhere. We stay on the two coding agents here; the knowledge-work hosts are their own course (see the ceiling).

Two families of coding-agent hosts. The Anthropic family — Claude Code, Claude Cowork, and claude.ai — shares one bundle format, the .claude-plugin bundle. The OpenCode family — OpenCode and OpenWork — shares the OpenCode plugin (a JS/TS module). A bundle stays inside its own family; the two don&#39;t cross. Underneath both sits SKILL.md, the portable lever: one file read natively by every host in both families, plus Codex. Keep a skill&#39;s body tool-agnostic and it travels everywhere; a whole bundle only travels within its family.

Concept 3: Bundle to share, not configure to keep

Both hosts let you customize without a plugin — Claude Code reads a .claude/ folder in your project; OpenCode reads .opencode/. That's the right tool when the customization is personal and lives in one repo. A plugin is what you reach for when the customization should travel: to your teammates, across your projects, to the community, with versions and updates.

So the test for "should this be a plugin?" is not what does it do — it's who else needs it. One developer, one project: a .claude/ folder. A team, many projects, or strangers: a plugin (invariant 1).

One consequence to know early: plugin skills and commands are namespaced by the plugin's name — a hello skill in a plugin called repo-tools is invoked as /repo-tools:hello. That prevents two installed plugins from clashing over the same name.

Checkpoint: the shape is in place. You know a plugin is a host-loaded unit, that two hosts package it differently, and that "plugin" means "customization made to share." Now the four levers.


Part 2: The capability levers

Three of the four levers add to what the agent can do. (The fourth, hooks, is different enough to get its own Part.)

The four levers a plugin uses to extend an agent. Three add capability: a skill is knowledge the agent uses by choice; a subagent is work it delegates to a fresh context; an MCP server is tools and data it reaches. The fourth is control: a hook is code that runs deterministically at fixed moments. Skills, subagents, and MCP servers are advisory — the model decides whether to use them; hooks are enforced — they run every time.

Concept 4: Skills — knowledge the agent uses by choice

A skill is a folder with a SKILL.md file: a description plus instructions. Claude reads the description and, when a task matches, pulls the skill in on its own — it's model-invoked. A skill is how you teach the agent how your team does a thing: your review checklist, your commit-message format, the steps of your release process.

---
description: Review a diff for our team's standards. Use when reviewing code or a PR.
---

When reviewing, check in this order:

1. Does it match the existing patterns in the file?
2. Error handling and edge cases.
3. Tests for the new behavior.
4. Security: secrets, input validation, injection.

The description is the most important line — it's what the model reads to decide whether the skill is relevant, so write it about when to use this, not just what it is. The body only loads when the skill fires, so it can be as long as it needs to be.

Because a skill is advice the model chooses to follow, it's the right lever for guidance — and the wrong lever for anything that must happen without fail (that's a hook, Concept 8).

Write a skill once, and every tool reads it. This is the skill's superpower as a lever: the SKILL.md format is shared across Claude Code, OpenCode, and Codex. To keep a skill portable, keep its body tool-agnostic — lean only on the frontmatter name and description, and don't reach for tool-specific constructs like $ARGUMENTS, allowed-tools, or disable-model-invocation. Then the same file works everywhere: Claude Code loads it from your plugin's skills/; OpenCode finds it natively in .opencode/skills/ or .claude/skills/; Codex in .agents/skills/. The starter's skills/loop-engineering/SKILL.md is a complete, portable example — read it.

Why frontmatter only?

Each host derives the skill's name and decides when to invoke it from the description. Beyond those two fields, the hosts diverge — what one supports, another ignores or chokes on. So anything you put in the body that's specific to one tool quietly breaks the skill on the others. Plain instructions in the body, two fields in the frontmatter, and the skill is universal.

Concept 5: Subagents — delegate with a fresh context

A subagent is a helper the main agent can hand a job to, with its own clean context window and its own instructions. You define it as a markdown file in the agents/ folder — frontmatter for its name and description, the body for its brief:

---
name: reviewer
description: Reviews a diff against our standards. Use after a change is written.
---

You review code in your own context. Check, in order: matches existing patterns,
error handling, tests, security. Report findings as a short ordered list.
Do not edit files — only review and report.

Delegation matters for two reasons: the subagent isn't distracted by the main conversation, and its work doesn't clog the main context.

Reach for a subagent when a task is self-contained and verifiable — "review this diff," "find everywhere this function is called," "write tests for this file." The main agent stays on the through-line; the subagent goes deep and reports back.

The mistake to avoid: making everything a subagent. Delegation has a cost (a fresh context has to be told what it needs). Use it when the focus and the clean slate are worth it, not for every small step.

Concept 6: MCP servers — the external reach a plugin can ship

A plugin can connect to an MCP server — the exact thing you built in Connector-Native Apps — via a .mcp.json file at the plugin root. It names the servers to connect; when the plugin is enabled, the host connects and the server's tools appear to the agent.

Wire it to a remote server you host — an HTTP URL, with the user's key in an auth header:

{
"mcpServers": {
"my-api": {
"type": "http",
"url": "https://api.yourdomain.com/mcp",
"headers": { "Authorization": "Bearer ${MY_API_KEY}" }
}
}
}

This is how a plugin gives the agent reach it didn't have: your internal API, a database, a service. And it's deliberate that the server is remote. A remote server keeps the logic, the data, and the secrets on infrastructure you control; the plugin ships only a pointer. That's what makes the reach durable — and, when you want it, gateable and sellable (Concept 10's monetization note). The plugin is a thin client; the value lives behind the URL.

Two ways to authenticate. Your server sits on the open internet, so it must check who's calling — and the course before this one (connector-native apps) used the stronger of the two:

  • A static bearer token (the example above) — an API key the user holds, carried in the header. Simplest, and it doubles as your subscription gate: your server checks the key and you revoke it when someone cancels. The pragmatic minimum for an internal or paid tool.
  • OAuth 2.1 — what real connectors use. You don't put a secret in .mcp.json; instead Claude Code's /mcp opens a browser, the user signs in, and the host stores the token. On the server side you act as an OAuth resource server: publish /.well-known/oauth-protected-resource so the client can find your auth server, validate the access token on every call, and check scopes per tool. If you set this up in the connector course, the same server works here unchanged — the plugin points at it and /mcp runs the login.

Build the server it points at. The full server is the connector course's deliverable, but here's the whole shape so your plugin has something real to aim at. A remote MCP server is an McpServer behind the Streamable HTTP transport (the local StdioServerTransport is the one we don't use), wrapped in a tiny web app that checks auth first:

import express from "express";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { z } from "zod";

const app = express();
app.use(express.json());

app.post("/mcp", async (req, res) => {
// 1) Auth gate FIRST. Bearer key here; swap in OAuth token validation for production.
const token = (req.headers.authorization ?? "").replace(/^Bearer /, "");
if (token !== process.env.AGENT_FACTORY_KEY) {
res.status(401).json({
jsonrpc: "2.0",
error: { code: -32001, message: "Unauthorized" },
id: null,
});
return;
}
// 2) One server + transport per request (stateless: simplest, scales horizontally).
const server = new McpServer({ name: "agent-factory", version: "1.0.0" });
server.registerTool(
"house_voice",
{
description: "Return the house writing rules.",
inputSchema: { topic: z.string().optional() },
},
async ({ topic }) => ({
content: [
{ type: "text", text: "House voice: declarative, short, no hedging." },
],
}),
);
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
res.on("close", () => {
transport.close();
server.close();
});
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
});

app.listen(3000);

Deploy that behind HTTPS, set AGENT_FACTORY_KEY per subscriber, and your .mcp.json points at the URL. Pin the SDK to v1.x (latest is v1.29.x; v2 is pre-alpha, stable expected Q3 2026). For the rest of the build — OAuth, the RAG behind the tool, hosting — that's Connector-Native Apps and the MCP authorization spec.

The starter ships this exact server in server/ — runnable, with server/test_server.mjs proving an unauthorized key is rejected and an authorized one can list and call the tool. The plugin itself ships only .mcp.json.example (the pointer); the server lives beside it, deployed separately.

Why not a local server?

Claude Code also supports local MCP servers (a command the host runs as a process on the user's machine). We skip them on purpose. A local server ships its code with the plugin and runs on someone else's computer — copyable like a skill, and a runtime you don't own. For a plugin, a remote server is almost always the right call: build and host it once, point any number of plugins at it by URL.

If you took the connector course, the link is direct: there you shipped an MCP server users paste into the chat app; here a plugin points the coding agent at the same server by URL. Same server, two front doors. One thing carries over either way: that server runs your code and holds your secrets — it's part of the trust you ship (invariant 4), and now your users are trusting it too.

Checkpoint: capability levers covered. Skills add knowledge, subagents add focused help, MCP servers add reach — and all three are advisory: the model decides when to use them. Next, the lever that isn't optional.


Part 3: The deterministic lever

Concept 7: Hooks — code that runs every time

A hook is a command the host runs automatically at a fixed point in the agent's lifecycle. It is not a suggestion to the model; it's your code, executed by the host, on a schedule the model can't change. That single property is why hooks matter more than they first appear.

The lifecycle points (events) fall into three cadences:

  • Once per session: SessionStart, SessionEnd.
  • Once per turn: UserPromptSubmit (before the agent sees a new prompt), Stop (when the agent finishes).
  • On every tool call: PreToolUse (before a tool runs — the only point that can block it) and PostToolUse (after).

A command hook works the same way every time: the host sends it a JSON description of the event on standard input; your script reads it, does its job, and signals back with an exit code.

  • Exit 0 — allow / done.
  • Exit 2 on PreToolUseblock the tool call. Whatever you print to standard error is handed to the model as the reason, so it can adjust.
  • Any other non-zero — a non-blocking error: it's logged, but the action still proceeds.

A reference card for hooks in two halves. WHEN A HOOK CAN FIRE: once per session (SessionStart, SessionEnd); once per turn (UserPromptSubmit, Stop); and on every tool call (PreToolUse, which can block, and PostToolUse). HOW A COMMAND HOOK ANSWERS: exit 0 allows and the tool runs as normal; exit 2 blocks the tool call on PreToolUse and hands stderr to the model as the reason; exit 1 only warns — it is logged but the action still proceeds, a common mistake for guards.

That exit-2 rule is the whole game for guardrails, and it's the most common thing people get wrong (exit 1 doesn't block). In a plugin, hooks live in hooks/hooks.json:

{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/format.sh"
}
]
}
],
"PreToolUse": [
{
"matcher": "Read|Edit|Write|Bash",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/block-secrets.sh"
}
]
}
]
}
}

The matcher is a pattern for which tools the hook fires on (Write|Edit = file writes; Bash = shell commands). The plugin-root path variable points at your plugin's folder so the host can find your scripts — confirm its exact name against your Claude Code version (the SDK and plugin paths move).

Concept 8: Must-always is a hook, not an instruction

Here is the most important idea in the course. Anything you write as instructions — a skill, a CLAUDE.md line — is advisory. The model usually follows it, but it can forget, run out of context, or decide the conversation has moved on. For most guidance, that's fine. For anything that must hold every single time, advice isn't enough. A hook is.

Two patterns carry most of the real value:

Format on write — PostToolUse. After every file edit, run your formatter. The model's output no longer has to be perfectly styled, because the formatter normalizes it every time:

#!/usr/bin/env bash
# hooks/format.sh — runs after every Write/Edit
path=$(jq -r '.tool_input.file_path // empty') # read the edited file's path from the event
[[ -n "$path" ]] && npx --yes prettier --write "$path" 2>/dev/null
exit 0

Block what must never happen — PreToolUse, exit 2. Inspect the tool call; if it crosses a line, block it and tell the model why. This guard covers both secret files and destructive commands, so it reads two fields from the event:

#!/usr/bin/env bash
# hooks/block-secrets.sh — runs before Read/Edit/Write/Bash
input=$(cat) # read the event ONCE (see note)
path=$(printf '%s' "$input" | jq -r '.tool_input.file_path // empty')
cmd=$(printf '%s' "$input" | jq -r '.tool_input.command // empty')

if [[ "$path" == *.env* || "$path" == */secrets/* ]]; then
echo "Blocked: $path is a secret file. Do not read or edit it." >&2 # stderr → the model
exit 2 # exit 2 → blocked
fi
if [[ "$cmd" == *"rm -rf"* || "$cmd" == *"git push --force"* ]]; then
echo "Blocked: refusing to run a destructive command ($cmd)." >&2
exit 2
fi
exit 0
The one-line gotcha: read the event ONCE

The host sends the event on standard input, and standard input is a stream — the first thing that reads it drains it. If you call jq twice directly (path=$(jq …) then cmd=$(jq …)), the first call consumes the whole event and the second gets nothing, so cmd is silently empty and your command guard never fires. Capture it once with input=$(cat) and parse that variable, as above. This is the exact bug the starter's test catches — and it would have shipped a guard that looks right and quietly fails on rm -rf.

A skill that says "never read .env" is a hope. This hook is a guarantee.

Run it. Paste this to your coding agent:

add a PostToolUse hook that formats files after Write/Edit, and a PreToolUse hook (matched on Read|Edit|Write|Bash) that blocks reads/edits of .env/secrets/ and blocks rm -rf in Bash, all with exit 2. Read the event once with input=$(cat). Then try to read my .env and run rm -rf and show me both are blocked, and edit a file and show me it got formatted.

(You build one here to see the mechanics. In the worked example you'll use the starter's already-proven version instead of rewriting it — building it once yourself is how you'll trust it.)

Run it yourself in a terminal (raw commands). Test a hook the way the host calls it — pipe it a fake event and check the exit code:

echo '{"tool_input":{"file_path":"/app/.env"}}' | ./hooks/block-secrets.sh; echo "exit: $?"   # expect: exit 2
echo '{"tool_input":{"command":"rm -rf /"}}' | ./hooks/block-secrets.sh; echo "exit: $?" # expect: exit 2
echo '{"tool_input":{"file_path":"/app/main.ts"}}' | ./hooks/block-secrets.sh; echo "exit: $?" # expect: exit 0

Verify. All three exit codes match: .env blocked (2), rm -rf blocked (2), normal file allowed (0). If the secret read goes through, your hook is exiting 1 instead of 2 — the single most common mistake. If rm -rf goes through but .env is caught, you're reading stdin twice (see the gotcha above). Exit 2, read once, or it doesn't block.

Checkpoint: the deterministic lever works. You can make something happen on every edit, and stop something on every tool call. That's the difference between a plugin that suggests and one that enforces.

When a hook misbehaves (the part guides skip)

Hooks run on every matching tool call, so a bad one is felt immediately. Four rules keep them out of your way:

  • Keep them fast. A PreToolUse hook gates every matching call, so slow logic (a network round-trip, a full test suite on each edit) stalls the agent. Aim for well under a second; do heavy work at Stop, not per call.
  • Fail safe, on purpose. Decide what happens when your hook itself errors. A formatter that can't run should exit 0 (let the edit stand) — that's why format.sh ends in exit 0 and swallows prettier's errors. A guard is the opposite: if it can't decide, prefer to block. Never let a crashing guard silently fall through to exit 0.
  • Say why, every time you block. A bare exit 2 with no message sends the model nothing to act on, and it will retry the same thing. The stderr line is the fix instruction — make it specific ("edit the source, not the generated file").
  • Debug it the way the host calls it. Pipe a fake event in and read the exit code (the raw-command test above). If a hook seems to fire too often, check your matcherWrite|Edit is narrow; an empty or broad matcher fires on everything.

Part 4: Ship it

Concept 9: The manifest and the structure

A Claude Code plugin is a folder described by a manifest, .claude-plugin/plugin.json. (Claude Code can auto-discover the standard component folders even without it, but ship the manifest — it carries your name, version, and description.)

{
"name": "agent-factory",
"description": "A portable skill, guard hooks, and a reviewer subagent.",
"version": "1.0.0",
"author": { "name": "Your Name" }
}

Everything else sits at the plugin root (not inside .claude-plugin/ — that's the one structural mistake people make):

agent-factory/
├── .claude-plugin/
│ └── plugin.json # the manifest (this, and only this, goes here)
├── skills/ # skills as <name>/SKILL.md
├── agents/ # subagent definitions
├── hooks/
│ └── hooks.json # event → command wiring
└── .mcp.json # optional: MCP servers to load

version matters for updates: when you bump it, installers get the new version; if you omit it and distribute via git, every commit counts as a new version. Run claude plugin validate before you share — the same check the marketplace review runs.

The tree above is the plugin itself, wherever it sits. In the starter it lives at plugins/agent-factory/ inside a marketplace repo — which is the next Concept.

Concept 10: Marketplaces — how a teammate gets it

A marketplace is just a git repository with a catalog file (marketplace.json) listing one or more plugins. That's the whole distribution story: you don't publish a package to a registry, you point people at a repo.

{
"name": "agent-factory",
"owner": { "name": "Your Name" },
"plugins": [
{
"name": "agent-factory",
"source": "./plugins/agent-factory",
"description": "A portable skill, guard hooks, and a reviewer."
}
]
}

A teammate then runs two commands inside Claude Code:

/plugin marketplace add your-org/agent-factory   # the git repo with marketplace.json
/plugin install agent-factory@agent-factory # plugin@marketplace
note

Don't confuse the two manifests. A plugin has .claude-plugin/plugin.json; a marketplace has .claude-plugin/marketplace.json. The documented layout keeps them apart: the marketplace.json sits at the repo root, and each plugin lives in its own subfolder (./plugins/<name>/) with its own plugin.json. The starter uses exactly this — plugins/agent-factory/. (One repo can be both a marketplace and host one plugin, but keeping the plugin in a ./plugins/<name> subfolder is the pattern every official example uses — prefer it.)

Pinning and sources — how updates actually work. A plugin's source can be a relative path (above) or an object pointing at another repository entirely; a marketplace can list plugins from many repos, each pinned independently:

{
"name": "code-formatter",
"source": { "source": "github", "repo": "acme/formatter", "ref": "v2.1.0" }
}

ref pins a branch or tag, sha pins an exact commit, and when both are set the sha wins. That — not the catalog file itself — is how a teammate gets a specific version and how you ship updates. (A url source covers GitLab and other git hosts; a local path is handy for testing.)

Two footguns worth knowing before you publish:

  • Relative paths only resolve when the marketplace is added via Git (GitHub/GitLab/git URL). If someone adds it by a direct URL to the marketplace.json file, ./plugins/... won't resolve — use a github or url source then.
  • Installed plugins are copied to a cache, so a plugin can't reach files outside its own folder with ../. Keep everything the plugin needs inside it; symlinks are followed during the copy as long as they point within the plugin — which is why the starter's two symlinks target the plugin's own files.

Anthropic runs its own official catalogs — claude-plugins-official (curated) and the community claude-plugins-community — so pick a distinct name for your own marketplace rather than shadowing those. (Naming and submission rules are young; confirm the current reserved-name policy against the plugins reference before you publish.)

Distributing to a team — or to non-coders. On Team and Enterprise plans, owners publish a marketplace from Organization settings → Plugins; a Knowledge Work marketplace is added by default, and the plugins you distribute appear in chat and in Claude Cowork — the same bundle reaching knowledge workers, not just builders (see the ceiling, and the Cowork and OpenWork course).

Run claude plugin validate before you share. The schema above is current as of mid-2026; re-verify field names against the Claude Code plugins reference before publishing, since this surface is young and moving.

Can you charge for it? Yes — but not for the files. A marketplace is a catalog, not a store: no payment layer, no license check, no per-seat gating, no subscription primitive anywhere in the format. Nothing stops you charging for your own plugins — people already run paid skills — but the marketplace gives you zero commerce infrastructure. Billing, auth, and access enforcement are yours to build, entirely outside the plugin system.

And there's a sharp edge specific to plugins: a skill is a plaintext SKILL.md file with no DRM. The instant a customer installs it, they hold your source. Gating static files behind a subscription invites exactly one churn event per customer — pay once, clone, cancel — and for a curriculum where the value is the readable text, selling the file gives away the IP on first download.

So the only model that supports a real subscription is hosted access: keep the valuable logic on a server you control and sell entry to the server, not the files. In plugin terms, the installed plugin is a thin, free client; the paid part is an API key that unlocks your hosted MCP server, gated by subscription status. The free SKILL.md courses become the funnel; the subscription rides on what can't be trivially copied — the hosted MCP server, the RAG retrieval whose quality is the moat, the live tutor — infrastructure you own that costs you per query and returns an outcome a clone can't.

This caveat is plugins-only. A connector-native app — the course before this one — doesn't have the problem, because it already is a pure hosted MCP server: nothing ships to the user but a paste-in link, the logic and data stay server-side, and access is gated server-side. That's exactly the monetizable layer this note steers you toward. Plugins put readable files on the user's disk; connector-native apps don't. The clean pattern is both together — the free plugin is the client and the funnel, and the subscription sits on the connector-native server behind it.

And the plugin wires that server directly. "Everything in the MCP server" is exactly what a plugin does: its .mcp.json points at your hosted HTTP server with the user's key in an auth header (the remote wiring from Concept 6), so the plugin ships only a pointer. The valuable code, the RAG, the data never leave your server; the installed plugin is a thin client; the key is the subscription gate — your server checks it, and a cancelled key stops working the moment you revoke it. That's the connector-native architecture delivered through the plugin channel instead of a paste-in link: same server, second front door. It cuts both ways, too — your users are now trusting your server with their requests, so the trust contract runs in both directions (Concept 11).

Two cautions before you bill for it
  • Don't break Anthropic's usage policies. If students consume Claude through your hosted service, how that usage is licensed and billed has to be legitimate — the same reasoning that makes routing a personal Pro/Max subscription into third-party tools a problem. If this turns into real revenue, read Anthropic's commercial terms directly (not a blog) and confirm the billing path for the Claude usage your server triggers.
  • Hosting means you own the security surface. A paid, gated MCP server is an attack target, and paying customers expect stability — so tool permissions and confirmations for any write/delete/network action stop being optional. That's invariant 4, now with money riding on it.
note

Human-only step. Creating the git repo and submitting to a marketplace are yours; your coding agent writes the files but can't open accounts or push in your name. So is the /plugin install your teammate runs — that's their trust decision (next Concept).

Concept 11: A plugin runs in the user's trust

Step back and see what a plugin can do on the machine that installs it: its hooks run shell commands, its bin/ is added to the path, its MCP servers run and reach out. Installing a plugin is running someone else's code. That cuts both ways, and both are your job:

  • As an author: least privilege. Only the hooks you need, matched as narrowly as possible. Don't reach for the network or the filesystem beyond the job. And make the trust legible — ship a README that states, in plain words, what the plugin does on the installer's machine:

    README.md — the trust contract
    What this plugin installs (skills, subagents, hooks, MCP servers)
    What hooks run, and when (e.g. PostToolUse formatter on Write/Edit)
    What files they inspect (e.g. reads tool_input.file_path; never opens .env)
    What commands they execute (e.g. prettier; no network calls)
    What network access they use (ideally: none)

    An installer who can read that in ten seconds can trust you in ten seconds.

  • As an installer: review before you install, the way you'd review a dependency. Prefer marketplaces you trust; read what the hooks do; remember a PreToolUse hook sees every tool call.

This is invariant 4, and it's not paperwork — a formatter hook that quietly uploaded your files would be invisible until you read it. Ship plugins you'd be comfortable installing.

Checkpoint: you can ship. Manifest, structure, a marketplace a teammate installs from, and a clear-eyed view of the trust you're shipping. One more host, then the full build.


Part 5: OpenCode plugins

Concept 12: OpenCode plugins — hooks as code

OpenCode takes the same ideas in a different form: a plugin is a JavaScript/TypeScript module that exports a function. The host calls your function with a context object and you return hooks — handlers for the agent's events. There's no separate manifest; you drop the file in .opencode/plugins/ (or install an npm package).

warning

Verify this section against your version. OpenCode's plugin API is younger and more code-level than Claude Code's declarative one, so the exact event names and signatures below (tool.execute.before, session.idle, the tool helper) move faster than the rest of this course. They're current as of mid-2026, but check them against your installed @opencode-ai/plugin before you teach or ship from this section.

A key difference from Claude Code: in OpenCode, a plugin is for hooks and tools — not for skills. OpenCode discovers skills natively from directories (.opencode/skills/, .claude/skills/, .agents/skills/), so your portable SKILL.md files need no plugin and no shim here at all. The plugin exists for the part that can't port — the hooks.

// .opencode/plugins/block-secrets.ts
import type { Plugin } from "@opencode-ai/plugin";

export const BlockSecrets: Plugin = async ({
project,
client,
$,
directory,
}) => {
return {
// runs before any tool — throw to block it (the OpenCode equivalent of exit 2)
"tool.execute.before": async (input, output) => {
if (input.tool === "read" && output.args.filePath?.includes(".env")) {
throw new Error("Blocked: .env files are off-limits.");
}
},
};
};

The mental model maps straight across from Claude Code: tool.execute.before is your PreToolUse, tool.execute.after is your PostToolUse, and throwing an error blocks the call the way exit 2 does. OpenCode also fires session and file events (session.idle, file.edited, and more), and a plugin can add a custom tool with the tool helper — the same "add reach" idea as an MCP server, written inline.

The context object hands you what you need: project and directory (where you are), $ (run shell commands), and client (talk to the agent, log). The form differs; the four invariants don't. A must-always rule is still a hook here — it's just a function that throws instead of a script that exits 2.


Part 6: A complete worked example — finish the agent-factory plugin

The starter is this plugin, half-built on purpose. The hooks ship complete (they're the part that must be right — you read and prove them); the review skill and reviewer subagent are stubs you finish; then you publish it and watch it travel. The prompts are the whole job — paste them into Claude Code, in order. The rhythm is the one you know: plan → review → execute → verify.

When you're done, agent-factory shows all four levers: a portable skill (loop-engineering, shipped) plus the review skill you write, a reviewer subagent, a format-on-write hook, a secret/command guard hook, a remote MCP server (runnable in server/, wired by URL via .mcp.json), and a marketplace entry.

0. Confirm the starter is sound. Run ./setup.sh, then bash plugins/agent-factory/tests/test_hooks.sh, then claude plugin validate. Paste:

Load the plugin with claude --plugin-dir ./plugins/agent-factory and confirm the loop-engineering skill is available as /agent-factory:loop-engineering. Report what loaded.

Done when: validate is green, the hook tests say ALL PASS, and the sample skill loads.

1. Read the hooks — don't rewrite them. Open hooks/block-secrets.sh and hooks/format.sh and read every line; these are the must-be-right part, so you verify them rather than trust them. Paste:

Walk me through hooks/block-secrets.sh line by line. Then prove it live: try to read .env (must be blocked), run rm -rf in Bash (must be blocked), and edit a .ts file (must come back formatted).

Done when: the secret read and the destructive command are refused, and a normal edit is auto-formatted — verified live, not assumed.

2. Plan what's left, then review it against the invariants. Paste:

Plan the two pieces left: the review skill (skills/review/SKILL.md) and the reviewer subagent (agents/reviewer.md). Show the plan first. Keep the skill body tool-agnostic — frontmatter name + description only, no $ARGUMENTS or allowed-tools, so it ports to OpenCode too.

Then check the plan yourself: Is every must-always rule already a hook (it is — the hooks shipped)? Is the skill body portable (invariant 2, the lesson of Concept 4)? Is anything reaching wider than its job (invariant 4)?

3. Build the capability levers. Paste:

Fill in skills/review/SKILL.md (a real review checklist; description written about when to use it) and agents/reviewer.md (a reviewer that works in its own context and only reports, never edits). Show me /agent-factory:review running on a sample diff.

Done when: /agent-factory:review is invocable by its namespaced name and the subagent reviews in a clean context.

4. Make it installable. Paste:

Confirm the repo-root .claude-plugin/marketplace.json lists the plugin at ./plugins/agent-factory, run claude plugin validate, and give me the exact two commands a teammate runs to add the marketplace and install it.

Done when: validate is green and you have the /plugin marketplace add + /plugin install agent-factory@agent-factory commands.

5. Prove it travels. Install it into a different project (or have a teammate install it) and confirm the guard hook blocks a .env read there too — without any per-project setup.

Done when: a second project is protected by the same hook through one install. That's the whole point of a plugin: the rule travels.

Notice the rhythm never changed: plan → review → execute → verify, with the hooks proven first because they're the part that must be right. The levers came after.


Part 7: The ceiling, and where it grows

Concept 13: The ceiling — and the bridges out

Feel the edge of what a plugin is. A plugin makes a builder's agent better — sharper, safer, more yours. But three things still aren't yours, and each names the next course.

The loop isn't yours. Your hooks fire around the host's loop; they don't run a loop of their own. A plugin can't wake up, pursue a goal across many steps on its own, or do a job while you sleep. When you want a worker that owns its loop, you write the agent — that's Build AI Agents, later on the path.

The identity isn't yours. Your plugin acts as whoever is running the host. It has no credential of its own and no way to act on someone's behalf with bounded, revocable authority. When an agent needs its own identity — and a person needs to delegate authority to it safely — that's AI Identity (built on Better Auth): own the sign-in, then give the agent scoped, time-boxed, human-approved access.

The reach is borrowed. A plugin can wire a remote MCP server, but the server itself — the durable, user-facing thing a stranger pastes into the chat app, with its own state and sign-in — is the connector-native app you built last course. Plugins and connectors point at the same server from two hosts; together they cover both.

But notice which way the limits run — and where it grows. Your plugin reaches past the coding agent. The bundle you just built isn't stuck in Claude Code or OpenCode: a .claude-plugin bundle also loads in Claude Cowork and claude.ai chat, and an OpenCode plugin also loads in OpenWork. So the guard or skill you wrote for a builder's agent can travel, unchanged, to a knowledge-worker's agent — the lawyer, the analyst, the ops lead. Where those knowledge-work hosts are the whole story rather than a bonus, that's its own course: Cowork and OpenWork.

You didn't waste a step. You learned to extend an agent deterministically and ship it to a team — the exact skills you'll reuse when the agent, and its identity, become yours.

The same skeleton, other plugins

agent-factory is one shape. The levers don't change; only the job does:

  • A house-style plugin — a skill carrying your writing or code conventions, a PostToolUse formatter, a reviewer subagent. (For a team that wants one consistent voice.)
  • A safety pluginPreToolUse guards for production targets, secrets, and destructive commands; nothing else. (Least privilege, invariant 4.)
  • A service plugin — a remote MCP server (.mcp.json) for your hosted API, plus a skill that teaches the agent how to use it.
  • A workflow plugin — a Stop hook that runs your test suite when the agent finishes, and a skill for your release steps.

Pick the one closest to a pain your team actually has — the build is the same as the worked example's.

Capstone

Ship a plugin of your own. Pick one real friction in how your team works with a coding agent. Build a plugin that fixes it with the right levers — at least one hook that enforces a must-always rule (exit 2 or a thrown error), and at least one capability lever (skill, subagent, or MCP server). Publish it to a marketplace and have someone else install it. Confirm the hook fires for them, in their project, with no extra setup.

1Your Work
2Get Your Score

Discuss with an AI. Question your scores.
Come back when you have your BEST evaluation.


Before you publish this page

This course sits on top of two fast-moving extension systems, so treat it as version-bound. Before publishing, fill in the matrix below and re-verify each item against those versions:

Tested against
Claude Code version: ______ Claude Code plugin docs checked: ______
OpenCode version: ______ OpenCode plugin docs checked: ______
Node / Bun version: ______ jq version: ______
@modelcontextprotocol/sdk (server/): ______ (pin v1.x; latest v1.29.x; v2 pre-alpha, ~Q3 2026)
Remote MCP transport (HTTP) + auth: ______ (bearer / OAuth)

A few specifics move on their own schedule — date them and re-verify:

  • Claude Code plugin structure and plugin.json schema: the component folders and manifest fields are stable in shape, but verify field names and the ${CLAUDE_PLUGIN_ROOT} path variable against the current plugins reference before publishing the build steps.
  • Hook events and the exit-2 block rule: the per-call/turn/session events and "exit 2 blocks on PreToolUse, exit 1 only warns" are current as of mid-2026 (Claude Code hooks reference) — re-confirm the event list, since it has grown.
  • marketplace.json schema and submission flow: Concept 10 documents the current schema (sources, ref/sha pinning, reserved names, the relative-path and copy-to-cache footguns). Re-confirm field names and the community-marketplace review steps before relying on them — this surface is the youngest.
  • OpenCode plugin API: the Plugin type, the event names (tool.execute.before/after, session.*), and the tool helper are current (OpenCode plugins docs) — verify against the installed @opencode-ai/plugin version.
  • Remote MCP server (server/) and wiring: the server uses @modelcontextprotocol/sdk v1.x (McpServer + StreamableHTTPServerTransport). Pin the version (v2 is pre-alpha, stable expected Q3 2026) and re-run node server/test_server.mjs after npm install in server/. On the client side, the .mcp.json HTTP form (type: "http", url, headers) and auth (bearer token / OAuth /mcp flow) are current as of mid-2026 (Claude Code MCP reference) — verify the plugin connects and the tools appear in /mcp. If you ship OAuth, re-confirm the protected-resource-metadata and token-validation steps against the MCP authorization spec.
  • The starter (plugins-crash-course-starter.zip): before publishing, run claude plugin validate and the worked example end to end against today's Claude Code and OpenCode.