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.