Your system prompt is a liability
Your domain knowledge has exactly one home, and it is loaded on every turn whether relevant or not. So it grows. Every behaviour change becomes a global change, and eventually nobody can edit what the agent does without touching the one string everything else depends on.

I have watched three separate teams reach the same point with this, at roughly the same size, and the symptoms are identical every time. The prompt is four thousand words. Nobody remembers why paragraph nineteen is there. Someone adds a line about refunds and the tone of every unrelated answer shifts. The file has one author who has left.
A skill is one procedure
Not a persona, not a policy document. One thing the agent does: how you handle a refund, how you triage a ticket. Its own instructions, its own files.
acme-support/
├── plugin.json # the manifest
├── mcp.json # optional: MCP servers this brings
└── skills/
└── refunds/
├── SKILL.md # frontmatter + instructions
├── references/ # loaded only if SKILL.md says to
└── scripts/---
name: refunds
description: How we issue a refund. Use whenever a customer asks for money back.
---
1. Check the order is under 90 days old.
2. Refund to the original payment method.That is a complete skill. It is shipped as an Agent Plugin, which is a portable format, so what you write works in any client that supports it.
The mechanism, which is the actual argument
Progressive disclosure. Each skill's name and description — about a hundred tokens — sit in the system prompt from the first step, so the agent knows what exists. The instructions load only when it decides the skill applies. The bundled files load only when the instructions say they are worth reading.
A hundred procedures cost about what three do at rest.
Which is the whole difference from a longer prompt. A monolithic prompt pays full price for every procedure on every turn, including the 97 that have nothing to do with the question — and pays a second, invisible cost in attention, because a model reading four thousand words of policy to answer "what are your opening hours" is being actively distracted.
Which makes the description the most important line you write
It is all the agent has when deciding. So name the trigger, not the topic.
"How we issue a refund. Use whenever a customer asks for money back" beats "Refund policy", and it is not close. The second one is a filing label. The first one tells the agent when to reach for it.
If you write one thing carefully in a skill, write that line.
Publishing, and the packaging exercise we removed
Three shapes are accepted, and only the first is a packaged plugin:
# A packaged plugin — read as written.
curl -X POST "$BASE/plugins" -H "Authorization: Bearer $KEY" -F file=@acme-support.zip
# A single SKILL.md. This is what a first procedure actually looks like.
curl -X POST "$BASE/plugins" -H "Authorization: Bearer $KEY" -F file=@SKILL.md
# A folder of skills, each part named by its path.
curl -X POST "$BASE/plugins" -H "Authorization: Bearer $KEY" \
-F "files=@refunds/SKILL.md;filename=refunds/SKILL.md" \
-F "files=@triage/SKILL.md;filename=triage/SKILL.md"A manifest is written for you when there isn't one. Requiring a plugin.json — with a $schema URL and a name obeying rules nobody has read — in order to publish one file of instructions is a packaging exercise standing in front of the feature. The manifest is what makes a plugin portable, so it still gets written; it just gets written for you, and what is stored is a proper archive you can pull back out. Ship your own and it is never overridden.
Two more things we got wrong first and fixed:
A skill is named by its frontmatter, not by its folder. The spec wants them to match and we say so when they do not — but the skill publishes under the name it declares, because that is the name the agent sees and the one a re-upload replaces. This used to be a refusal, which meant a skill in any folder failed to publish. Nobody names a folder after the skill inside it.
What was skipped comes back in warnings. A skill that would not parse, an MCP server that was refused, a file named nearly SKILL.md — the upload succeeds with the rest and names what it left out. A 201 that quietly published less than you sent is the worst possible answer, because you go and test the thing that is not there.
(A second skill is a second directory. A file called anything other than exactly SKILL.md is an ordinary bundled file.)
There is no capability for using the skills you publish
Deliberately. A project that publishes a procedure wants its agent to read it, so a toggle for that would exist only to be found — and a customer whose plugin was silently ignored would have no way to discover why.
Callers who want to skip or narrow them do it per turn:
await ai.chat.send({ message, plugins: ["acme-support"] });It narrows only, and a name that is not visible is simply absent rather than an error. A pinned list should not start failing turns the day somebody deletes a plugin.
Who can upload
| You, with a project key | published to the whole project |
| Your end-users, with their own token | only with plugins:write. Private to them, unioned on top of yours |
An end-user's plugins are visible exactly the way their documents are — same rules, same scoping — because a plugin is a thing a person uploaded and there is no reason it should behave differently.
Only a project key can publish to everyone. One end-user changing how the agent behaves for the whole workspace is not a thing to leave to a capability toggle.
stdio servers are refused
If a plugin brings an mcp.json, its servers join the project's own through the same gate every other tool goes through — except stdio ones, which are refused outright.
A stdio server is a command line. A plugin may have come from a file an end-user uploaded. Running one would be arbitrary execution on the host with none of the sandbox's isolation.
The refusal is reported on the plugin rather than being silent, because a plugin whose tools never appear and never says why is the failure people spend an afternoon on. A skill's scripts/ are fine — they are files the agent may run in the sandbox, under the same path and capability rules as anything else it writes there.
