Part 5: Loading `SKILL.md`

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

The notebook starts small: 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. The teaching version keeps the happy path visible.

Call it:

list_skills()

You should see coding_standards, counter, deploy_app, hello, and 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

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:

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 lets the agent load a skill, see paths to scripts and templates, 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 (0)

Sign in to ask questions