Part 5: Loading `SKILL.md`

The skills are on disk. Now we need Python code that reads them, extracts the frontmatter, and returns content the agent can use.

In the notebook you start small with the name, description, and markdown body, which is enough to implement lazy loading.

A Skill data class

Create a tiny container for one loaded skill:

from dataclasses import dataclass

@dataclass
class Skill:
    name: str
    description: str
    content: str

This class keeps the loader code honest. Every part of the agent can pass around one Skill object instead of a loose dict with unknown keys.

Parse frontmatter

Set the skills directory:

from pathlib import Path

skills_dir = Path("skills/")
name = "hello"

The main file for a skill is always:

skill_file = skills_dir / name / "SKILL.md"

Use python-frontmatter to read the YAML block and markdown body:

import frontmatter

parsed = frontmatter.load(skill_file)

The parser gives you two pieces:

  • parsed.metadata - fields from the YAML frontmatter.
  • parsed.content - markdown after the frontmatter.

Wrap those pieces into the Skill object:

skill = Skill(
    name=parsed.metadata.get("name", name),
    description=parsed.metadata.get("description", ""),
    content=parsed.content,
)

This fallback keeps the directory name useful even if a small example skill omits the name field. A stricter real loader should validate that the field is present and matches the folder name.

A function version

Before creating a class, build the same logic as a function:

def load_skill(name: str) -> Skill | None:
    skill_file = skills_dir / name / "SKILL.md"
    if not skill_file.exists():
        return None

    parsed = frontmatter.load(skill_file)

    return Skill(
        name=parsed.metadata.get("name", name),
        description=parsed.metadata.get("description", ""),
        content=parsed.content,
    )

Test it with hello:

load_skill("hello")

The output should contain the skill name, the greeting description, and the markdown body.

Listing all skills

To give the agent a list of available skills, scan each subdirectory:

def list_skills() -> list[Skill]:
    skills = []

    if not skills_dir.exists():
        return skills

    for skill_dir in sorted(skills_dir.iterdir()):
        if not skill_dir.is_dir():
            continue

        skill = load_skill(skill_dir.name)
        if skill:
            skills.append(skill)

    return skills

This treats every subfolder as a skill folder. A real loader should add error handling, but the teaching version keeps the happy path visible.

Call the listing function:

list_skills()

You should see the five example skills:

  • coding_standards
  • counter
  • deploy_app
  • hello
  • joke

The SkillLoader class

The class version puts the same operations behind one object:

class SkillLoader:

    def __init__(self, skills_dir: Path | str = None):
        self.skills_dir = Path(skills_dir)

    def load_skill(self, name: str) -> Skill | None:
        skill_file = self.skills_dir / name / "SKILL.md"
        if not skill_file.exists():
            return None

        parsed = frontmatter.load(skill_file)

        return Skill(
            name=parsed.metadata.get("name", name),
            description=parsed.metadata.get("description", ""),
            content=parsed.content,
        )

The class also lists skills:

    def list_skills(self) -> list[Skill]:
        skills = []

        for skill_dir in sorted(self.skills_dir.iterdir()):
            if not skill_dir.is_dir():
                continue

            skill = self.load_skill(skill_dir.name)
            skills.append(skill)

        return skills

Then it creates the short listing we'll inject into the agent prompt:

    def get_description(self) -> str:
        skills = self.list_skills()

        skills_listing = "\n".join(
            f"  - {s.name}: {s.description}"
            for s in skills
        )

        return skills_listing

Create and use the loader:

skill_loader = SkillLoader(skills_dir)
print(skill_loader.get_description())

The result should look like this:

  - coding_standards: Use this when writing or reviewing code - provides project-specific coding standards
  - counter: Count things or list items with numbers
  - deploy_app: Deploy the application using deployment scripts and templates
  - hello: Skill for ALL greeting requests
  - joke: Skill for ALL joke requests

With this description, the model can choose a skill without loading all skill bodies into the prompt.

Resolving extra files in the prototype

The prototype loader adds one important feature: resolving @filename references inside skill content. In prototype/src/skills.py, load() calls _resolve_file_references() before returning the tool result.

This is prototype code you import rather than type:

  • load(name) looks up the skill, resolves its file references against the skill's own directory, and returns a SkillToolResult.
  • _resolve_file_references(content, base_dir) replaces references like @scripts/deploy.sh with a concrete path when that file exists, and leaves the reference untouched otherwise.

The full source is in the code repo at agent-skills/prototype/src/skills.py.

This exposes script and template paths from the loaded skill. The agent can then use its normal read_file or bash_command tools to read or run those files.

Continue with Part 6: The skill tool and prompt injection to make the loader available as an agent tool.

Questions & Answers

Sign up to ask questions, track your progress, and get access to other workshops · Already have an account? Sign in