A skill is data, not code

An agent skill -- in the sense that's spread from Claude Code's .claude/skills/ convention to Cursor's project rules, Codex's AGENTS.md, and Windsurf's .windsurfrules -- is a plain text file: a short YAML frontmatter block (a name and a description) followed by markdown instructions. It contains no executable logic. It cannot call an API, read a file, or run a subprocess on its own. Everything a skill does, it does by being read into an LLM's context and followed as an instruction, the same way a human employee follows a runbook.

That single design choice -- skill as inert text, never as code -- is the whole architecture. Every property people actually want from a "skills" system (portability across tools, independent versioning, safe composition, auditability) falls out of keeping the skill decoupled from the runtime that executes it. This article works through why that separation matters, how the discovery/loading mechanism that makes it useful actually works, and where the pattern breaks down if you get the boundary wrong.

Advertisement

The coupling problem this pattern solves

Before portable skill files, the default place to put agent behavior was the system prompt: one large block of instructions, owned by whoever built the agent, baked into that specific application. This works until you need the same behavior in a second place -- a different tool, a different team's agent, a different host entirely -- and discover the instructions are welded to that first system prompt's assumptions about available tools, response format, and surrounding context.

The failure mode is familiar from software generally: logic that should be a reusable module ends up copy-pasted and drifting. A code-review methodology written into one product's system prompt gets copied into a second product's system prompt, and six months later the two have quietly diverged -- one catches a class of bug the other doesn't, and nobody notices until a customer does. The fix in software is the same fix here: extract the reusable part into its own artifact with a stable interface, and let multiple runtimes depend on that artifact instead of on each other's internals.

A skill file is that extraction. It has exactly two things a runtime needs to know about it from the outside -- a name and a description -- and everything else is opaque instruction text the runtime injects verbatim. The runtime doesn't parse the skill's logic, branch on it, or need to understand what's inside. That opacity is a feature: it's what lets a skill move between runtimes that share nothing else.

The three things decoupling actually buys

Portability. Because a skill only requires a host that can read text and inject it into a model's context, the same file works anywhere that minimal contract holds. This isn't a hypothetical: this site's own code-review skill ships with install notes for four different tools, and the file itself doesn't change between them -- only where you save it changes.

Independent versioning. Because the runtime treats a skill as an opaque dependency rather than inlined logic, a skill can be rewritten, re-scoped, or fixed without touching the runtime or any other skill. This is the same benefit a shared library gets from being a separate package instead of vendored, pasted-in code: one version bump, one changelog entry, every consumer picks it up the same way.

Composability without coordination. A runtime that has fifty skills installed doesn't need fifty special cases in its own code -- it needs one generic mechanism (match a request against each skill's description, load the best match's body into context) that works identically whether there are five skills or five hundred. The skills themselves don't need to know about each other either. Compare this to a monolithic system prompt trying to cover fifty scenarios: every addition risks interacting with every existing instruction, because they all share one undifferentiated block of text.

How discovery actually works

The mechanism is closer to a plugin registry than to a router with hardcoded routes. At the point a request comes in, the runtime doesn't run a lookup table of if intent == X: use skill Y -- that would reintroduce exactly the coupling the pattern exists to avoid, since every new skill would require editing the runtime's own code. Instead, each skill advertises itself with a natural-language description, and the runtime (or an LLM call the runtime makes on its purpose) matches the current request against the set of available descriptions and pulls in the body of whichever skill's description best matches.

---
name: code-review
description: Review a diff, PR, or set of changed files for correctness
  bugs, security issues, and unnecessary complexity. Use when the user
  asks to review code, check a PR, or "look over" a change before it
  merges -- not for general code questions.
---
# the markdown instruction body follows here, injected into context
# only when the description above matches the current request

Two things make this work reliably in practice rather than degrading into random matches. First, the description has to be specific about and about what the skill is not for -- notice the negative clause above ("not for general code questions") is doing real work, narrowing the match so the skill doesn't fire on every mention of the word "code." Second, the matching step is cheap relative to running the skill itself: it only needs to decide relevance, not execute anything, so a runtime can afford to check a request against a large catalog of skill descriptions before committing context budget to loading any one skill's full body.

This is also why a skill's description field matters as much as its instruction body, and why it's the one piece of metadata every implementation of this pattern treats as load-bearing rather than cosmetic.

Where the boundary actually sits

Decoupling only helps if you draw the line in the right place. Three adjacent patterns are easy to confuse with a skill, and confusing them is where teams get the architecture wrong.

A skill is not a tool. A tool is a function the runtime can actually call -- it has a schema, it executes code, it returns structured data. A skill has none of that; it's instructions for how the model should behave, often including when and how to call tools, but the skill itself never runs anything. Conflating the two leads to skill files that try to embed executable logic in markdown, which the runtime then has to somehow parse and execute -- reintroducing exactly the runtime-specific coupling the pattern was built to avoid.

A skill is not a fine-tune. Fine-tuning changes the model's weights; a skill changes what's in the model's context window for one request. That difference has a direct practical consequence: a skill can be edited and take effect on the very next request, with a diff you can review before merging, while a fine-tune requires a training run, an evaluation pass, and a deployment before anything changes. Reach for a skill when the behavior is expressible as instructions a capable model can follow; reach for fine-tuning only when no amount of in-context instruction reliably produces the behavior you need (a narrow output format at very high volume and very low latency tolerance is the usual case).

A skill is not the system prompt. The system prompt is the runtime's own, always-on identity and constraints -- it's owned by whoever built the agent and shouldn't change per-request. Skills are supplementary, loaded conditionally, and owned independently of the runtime. A system prompt that has grown to include instructions for forty different scenarios is a system prompt that should be forty skills instead.

A minimal reference architecture

Stripped to its essential pieces, a skill-capable agent runtime has four components, and the diagram below shows how a request moves through them.

Request arrives → matched against the skill catalog → body injected → model runsUser request"review this diff"Skill catalogN description strings(name + description only)Matcherbest-match ondescription textSkillbody.mdModel context window this requestsystem prompt + skill body + user turn-- skill instructions never touch the runtime's own codeinject body
Skill loading: the catalog holds only metadata; a matched skill's body is injected into context for that one request and never touches the runtime's own code path.

Notice what's deliberately absent: there's no step where the runtime modifies its own behavior based on which skill loaded. The skill's instructions live entirely inside the model's context for that request; the runtime's code path is identical whether zero skills matched or three did. That invariant is what keeps adding the fifty-first skill from being able to break the other forty-nine -- it can only ever add a new possible context, never change how the runtime itself processes a request.

Advertisement

A skill is inert text with exactly one contractual surface -- name and description -- and everything else is opaque to the runtime. That's not a limitation; it's the entire reason the same file installs into four different tools unchanged, why one skill can be rewritten without touching another, and why the runtime's own code never has to grow to accommodate the next skill someone adds.