On 22 October the Agentic AI Foundation, the Linux Foundation group that already hosts MCP and AGENTS.md, took in four new projects at AGNTCon in San Jose. One of them is Anthropic's Agent Skills, the folder format Claude uses to pick up new know-how on demand. The foundation now hosts 10 projects, up from three at launch in December 2025.
That matters for anyone building their own agent. A skill written once can now be loaded by any harness that follows the spec, not only Claude. So I wrote a loader for my own agents, pointed it at five realistic business skills, and measured what progressive disclosure actually saves against the usual approach of pasting every instruction into the system prompt.
Short version: 56 to 81% fewer prompt tokens per task with five skills, and the gap grows to about 9x by 50 skills. Everything below is code I ran, with the output pasted as it came out.
The format in 60 seconds
A skill is a folder with a SKILL.md file. The file starts with YAML frontmatter and continues with Markdown instructions. From the spec at agentskills.io:
-
nameis required: 1 to 64 characters, lowercase letters, digits and hyphens, no leading, trailing or double hyphens, and it must match the folder name. -
descriptionis required: 1 to 1024 characters, saying what the skill does and when to use it. -
license,compatibility(max 500 characters),metadata(string to string map) and an experimentalallowed-toolsare optional. - Optional folders:
scripts/,references/,assets/.
Loading happens in three levels. Level 1 is the name and description of every skill, about 100 tokens each, always in the system prompt. Level 2 is the full SKILL.md body, loaded when the agent decides the skill is relevant (the spec recommends under 5,000 tokens and 500 lines). Level 3 is any referenced file, read only when a step needs it.
Here is the top of one of my five test skills:
---
name: refund-requests
description: "Decide and draft replies for customer refund, cancellation and chargeback requests. Use when a customer asks for money back, wants to cancel a subscription, or disputes a charge."
metadata:
owner: support
version: "1.2"
---
# Refund requests
## Steps
1. Find the order or subscription by the customer's email. If there is more than one match, ask which one.
2. Work out the purchase date, plan, amount paid and how much of the service was used.
3. Apply the rules in references/POLICY.md in order. Stop at the first rule that matches.
4. If the refund is under 200 USD and a rule says approve, issue it with the billing tool and say so.
5. If the refund is 200 USD or more, or no rule matches, do not issue anything. Draft a reply and put the ticket in the "needs human" queue with a one-line reason.
6. For chargebacks, never argue with the customer. Collect the order ID, the date and the last four digits of the card, then hand off to billing.
The policy table lives in references/POLICY.md, so the agent only reads it when a refund actually needs a rule.
The loader: 113 lines
It does four things: validates each skill against the spec, builds the level 1 catalog, serves levels 2 and 3 through two tools, and counts tokens so you can see the difference.
"""Minimal Agent Skills loader: validate, build the catalog, load on demand, measure tokens."""
import json, re, sys
from pathlib import Path
import yaml # pip install pyyaml
import tiktoken # pip install tiktoken (token counts are an approximation for non-OpenAI models)
ENC = tiktoken.get_encoding("o200k_base")
NAME_RE = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$") # lowercase, digits, single hyphens, no leading/trailing hyphen
def tokens(text: str) -> int:
return len(ENC.encode(text))
def parse_skill(skill_md: Path):
raw = skill_md.read_text()
m = re.match(r"^---\n(.*?)\n---\n(.*)$", raw, re.S)
if not m:
raise ValueError(f"{skill_md}: missing YAML frontmatter")
return yaml.safe_load(m.group(1)) or {}, m.group(2)
def validate(skill_dir: Path) -> list[str]:
errors = []
md = skill_dir / "SKILL.md"
if not md.exists():
return [f"{skill_dir.name}: no SKILL.md"]
try:
meta, body = parse_skill(md)
except ValueError as e:
return [str(e)]
name, desc = str(meta.get("name", "")), str(meta.get("description", ""))
if not 1 <= len(name) <= 64 or not NAME_RE.match(name):
errors.append(f"{skill_dir.name}: bad name {name!r}")
if name != skill_dir.name:
errors.append(f"{skill_dir.name}: name {name!r} must match the folder name")
if not 1 <= len(desc) <= 1024:
errors.append(f"{skill_dir.name}: description must be 1 to 1024 chars (got {len(desc)})")
if "compatibility" in meta and not 1 <= len(str(meta["compatibility"])) <= 500:
errors.append(f"{skill_dir.name}: compatibility must be 1 to 500 chars")
if "metadata" in meta and not all(isinstance(v, str) for v in (meta["metadata"] or {}).values()):
errors.append(f"{skill_dir.name}: metadata values must be strings")
if body.count("\n") > 500:
errors.append(f"{skill_dir.name}: SKILL.md body over 500 lines, move detail to references/")
if tokens(body) > 5000:
errors.append(f"{skill_dir.name}: body is {tokens(body)} tokens, spec recommends under 5000")
return errors
class SkillStore:
def __init__(self, root: str):
self.root = Path(root).resolve()
self.skills = {}
for d in sorted(p for p in self.root.iterdir() if p.is_dir()):
errs = validate(d)
if errs:
print("SKIP", *errs, sep="\n ", file=sys.stderr)
continue
meta, body = parse_skill(d / "SKILL.md")
self.skills[meta["name"]] = {"dir": d, "meta": meta, "body": body}
def catalog(self) -> str:
"""Level 1: only name + description go into the system prompt."""
rows = [f'<skill name="{n}">{s["meta"]["description"]}</skill>' for n, s in self.skills.items()]
return ("<available_skills>\n" + "\n".join(rows) + "\n</available_skills>\n"
"Call load_skill(name) before doing a task that matches a skill.")
def load_skill(self, name: str) -> str:
"""Level 2: the full SKILL.md body, loaded when the model asks for it."""
if name not in self.skills:
return f"Unknown skill {name!r}. Available: {', '.join(self.skills)}"
return self.skills[name]["body"]
def read_skill_file(self, name: str, path: str) -> str:
"""Level 3: a referenced file, resolved inside the skill folder only."""
base = self.skills[name]["dir"]
target = (base / path).resolve()
if not target.is_relative_to(base) or not target.is_file():
return f"No file {path!r} in skill {name!r}"
return target.read_text()
def eager_prompt(self) -> str:
"""What many agents do today: paste every instruction and reference up front."""
parts = []
for s in self.skills.values():
parts.append(s["body"])
parts += [f.read_text() for f in sorted(s["dir"].rglob("*")) if f.is_file() and f.name != "SKILL.md"]
return "\n\n".join(parts)
TOOLS = [ # Anthropic-style tool definitions; the same shape maps to OpenAI function tools
{"name": "load_skill", "description": "Load the full instructions of a skill by name.",
"input_schema": {"type": "object", "properties": {"name": {"type": "string"}}, "required": ["name"]}},
{"name": "read_skill_file", "description": "Read a file referenced by a loaded skill, e.g. references/POLICY.md.",
"input_schema": {"type": "object", "properties": {"name": {"type": "string"}, "path": {"type": "string"}},
"required": ["name", "path"]}},
]
if __name__ == "__main__":
store = SkillStore(sys.argv[1] if len(sys.argv) > 1 else "skills")
catalog, eager = store.catalog(), store.eager_prompt()
tool_tokens = tokens(json.dumps(TOOLS))
print(f"skills loaded: {len(store.skills)}")
print(f"catalog (level 1): {tokens(catalog)} tokens + {tool_tokens} for the two tool definitions")
print(f"eager prompt (everything): {tokens(eager)} tokens\n")
# Three real tasks and what each one actually needs to read
tasks = {
"refund for a duplicate charge": [("refund-requests", None), ("refund-requests", "references/POLICY.md")],
"book a call with a lead": [("meeting-scheduler", None)],
"enter an AWS invoice": [("invoice-extraction", None), ("invoice-extraction", "references/VENDORS.md")],
}
price_in = 2.00 # USD per million input tokens, Claude Sonnet 5.5 list price
for task, reads in tasks.items():
loaded = sum(tokens(store.load_skill(n) if p is None else store.read_skill_file(n, p)) for n, p in reads)
prog = tokens(catalog) + tool_tokens + loaded
print(f"{task:32s} progressive {prog:5d} eager {tokens(eager):5d} saved {1 - prog / tokens(eager):.0%}")
print("\npath check:", store.read_skill_file("refund-requests", "../invoice-extraction/SKILL.md"))
print("unknown:", store.load_skill("pricing"))
pip install pyyaml tiktoken, then python3 skills_loader.py skills:
skills loaded: 5
catalog (level 1): 278 tokens + 125 for the two tool definitions
eager prompt (everything): 3020 tokens
refund for a duplicate charge progressive 1330 eager 3020 saved 56%
book a call with a lead progressive 564 eager 3020 saved 81%
enter an AWS invoice progressive 1176 eager 3020 saved 61%
path check: No file '../invoice-extraction/SKILL.md' in skill 'refund-requests'
unknown: Unknown skill 'pricing'. Available: invoice-extraction, lead-qualification, meeting-scheduler, refund-requests, sql-weekly-report
The catalog for five skills is 278 tokens, plus 125 for the two tool definitions. A refund task loads the refund skill and its policy table and lands at 1,330 tokens. Booking a call only needs the scheduler skill: 564 tokens. Pasting everything costs 3,020 tokens on every single call, whatever the task.
The path check matters in practice: a model asked to read references/POLICY.md will sometimes invent a path, and the loader should answer with a clear miss rather than reading whatever file the string points at.
How it scales
Five skills is a toy. Real support or ops agents end up with dozens. I took the per-skill averages from my five and extrapolated:
"""Extrapolate catalog vs eager size as the skill library grows, and show the validator catching bad skills."""
import tempfile
from pathlib import Path
from skills_loader import SkillStore, validate, tokens
store = SkillStore("skills")
n = len(store.skills)
per_skill_catalog = (tokens(store.catalog()) - 30) / n # ~30 tokens of wrapper text
per_skill_eager = tokens(store.eager_prompt()) / n
skill_content = 620 # mean SKILL.md + reference tokens one task loaded in skills_loader.py: (1330+564+1176)/3 - 403
price = 2.00 # USD per million input tokens, Claude Sonnet 5.5
print(f"per skill: catalog {per_skill_catalog:.0f} tokens, eager {per_skill_eager:.0f} tokens\n")
print(f"{'skills':>6} {'catalog':>8} {'eager':>8} {'eager $/1k calls':>17} {'progressive $/1k calls':>23}")
for k in (5, 20, 50, 100, 300):
cat = 30 + per_skill_catalog * k
eager = per_skill_eager * k
prog = cat + 125 + skill_content # catalog + tool definitions + one task's skill content
print(f"{k:6d} {cat:8.0f} {eager:8.0f} {eager*price/1e6*1000:17.2f} {prog*price/1e6*1000:23.2f}")
with tempfile.TemporaryDirectory() as tmp:
bad = {"Refund_Bot": "---\nname: Refund_Bot\ndescription: refunds\n---\nbody\n",
"crm--sync": "---\nname: crm--sync\ndescription: ''\n---\nbody\n",
"pdf-tools": "---\nname: pdf-processing\ndescription: Work with PDFs.\n---\nbody\n"}
for folder, text in bad.items():
d = Path(tmp) / folder; d.mkdir(); (d / "SKILL.md").write_text(text)
print(*validate(d), sep="\n")
per skill: catalog 50 tokens, eager 604 tokens
skills catalog eager eager $/1k calls progressive $/1k calls
5 278 3020 6.04 2.05
20 1022 12080 24.16 3.53
50 2510 30200 60.40 6.51
100 4990 60400 120.80 11.47
300 14910 181200 362.40 31.31
Refund_Bot: bad name 'Refund_Bot'
crm--sync: bad name 'crm--sync'
crm--sync: description must be 1 to 1024 chars (got 0)
pdf-tools: name 'pdf-processing' must match the folder name
At 50 skills the eager prompt is about 30,000 tokens on every call. The progressive version is about 3,250, because the catalog grows by about 50 tokens per skill while a task still loads one skill's content. At 300 skills the eager prompt is 181,000 tokens, which is past where most teams would ever send it, and even the catalog reaches about 15,000 tokens. That is the point where you add a search step that picks the 10 most likely skills before the model sees the catalog.
The validator output at the bottom is the other half of the value. Uppercase names, double hyphens, empty descriptions and folder mismatches all get rejected before an agent ever sees them, which saves a confusing debugging session later.
Wiring it into your agent loop
The two tools are plain JSON schemas, so they drop into any tool-calling loop. The dispatch is a few lines:
store = SkillStore("skills")
system = BASE_PROMPT + "\n\n" + store.catalog()
def handle_tool(name, args):
if name == "load_skill":
return store.load_skill(args["name"])
if name == "read_skill_file":
return store.read_skill_file(args["name"], args["path"])
return f"unknown tool {name}"
Pass TOOLS alongside your own tools, put the catalog in the system prompt, and append each tool result to the conversation as usual. The model reads the descriptions, calls load_skill when a task matches, and asks for reference files when the instructions point to them.
Honest caveats
-
Token counts are approximate. I used tiktoken's
o200k_base. Claude uses a different tokenizer, and Anthropic's pricing page notes that Claude 4.7 and later models produce about 30% more tokens for the same text. The ratios hold; the absolute numbers move. - Prompt caching narrows the dollar gap. On Claude Sonnet 5.5 a cache read costs 0.10 USD per million tokens against 2.00 for fresh input, so a stable eager prompt that stays cached is about 20 times cheaper than my table shows. It is still a longer context on every call, which costs latency, and every edit to any skill invalidates the cache for all of them.
- Each skill load is an extra round trip. One more model turn per task. For voice agents, where every 100 ms counts, preload the one or two skills a call almost always needs.
- Descriptions do all the routing. The model only sees name and description until it loads a skill. A vague description such as "Helps with PDFs" means the skill never fires. Put the trigger words your users actually say into it.
- Skills are not tools. A skill is instructions and files. If a step needs to touch an API, that is still a tool or an MCP server, and the skill tells the agent when and how to use it.
Where this fits
Most agent projects I see start with one giant system prompt and hit a wall around the tenth workflow, when instructions start contradicting each other and every call carries all of them. Moving to skills early keeps each workflow testable on its own and lets non-engineers edit a Markdown file instead of a prompt buried in code.
At Axionry we build production agents this way: skills for know-how, MCP for tools, evals per workflow, and cost per task on a dashboard from day one. A working agent demo ships in under two weeks, and production AI agents are capped at 5K USD, with exact pricing set once we talk. Have a look at how we build custom AI agents, our notes on MCP in production, or book a call.
Sources: Linux Foundation: AAIF announces Agent Skills, Open Responses, OpenEnv and OpenSandbox; Agent Skills specification; Anthropic Engineering: Equipping agents for the real world with Agent Skills; Anthropic API pricing.
Top comments (0)