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_standardscounterdeploy_apphellojoke
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.