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.

The notebook starts small with the name, description, and markdown body. That 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 assumes each subfolder is 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 will 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

This description is what lets the model 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.

The method looks like this:

def load(self, name: str) -> SkillToolResult:
    skill = self.get(name)
    if skill is None:
        raise ValueError(f"Skill not found: {name}")

    base_dir = self.skills_dir / name
    content = self._resolve_file_references(skill.content, base_dir)

    return SkillToolResult(
        name=skill.name,
        description=skill.description,
        content=content,
    )

The resolver replaces references like @scripts/deploy.sh with a concrete path if that file exists:

def _resolve_file_references(self, content: str, base_dir: Path) -> str:
    def replace_ref(match):
        filename = match.group(1)
        full_path = base_dir / filename
        if full_path.exists():
            return str(full_path)
        return match.group(0)

    return re.sub(r"@([^\s,)]+)", replace_ref, content)

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 in to ask questions