DEV Community

Cover image for Agent Skills Is Now a Linux Foundation Standard. My 113-Line Loader Cuts Prompt Tokens 56 to 81%
Neeraj Sharma
Neeraj Sharma

Posted on AI-assisted

Agent Skills Is Now a Linux Foundation Standard. My 113-Line Loader Cuts Prompt Tokens 56 to 81%

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:

  • name is required: 1 to 64 characters, lowercase letters, digits and hyphens, no leading, trailing or double hyphens, and it must match the folder name.
  • description is 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 experimental allowed-tools are 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.
Enter fullscreen mode Exit fullscreen mode

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"))
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

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}"
Enter fullscreen mode Exit fullscreen mode

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)