How Reasonix Parses Frontmatter for Skills and Commands: A Deep Dive into the YAML Pipeline
Reasonix parses skill and command metadata from YAML frontmatter blocks using a three-stage pipeline: split the file on --- fences, unmarshal YAML into a normalized map[string]string with lower-cased keys, then let skill- or command-specific loaders extract the fields they need.
Reasonix, the extensible AI agent framework in the esengine/DeepSeek-Reasonix repository, relies heavily on YAML frontmatter to declare metadata for both skills (reusable AI capabilities) and commands (CLI/desktop UI actions). This article breaks down exactly how the parsing pipeline works, from the low-level frontmatter.Split function through the specialized loaders that build runtime structs.
The Frontmatter Parsing Pipeline
The entire system centers on internal/frontmatter/frontmatter.go, which provides a reusable, permissive parser. The pipeline follows three clear stages:
Stage 1: Splitting the File on Fences
The frontmatter.Split function scans for a leading --- delimiter. When found, everything between the opening and closing fence becomes raw YAML; everything after the closing fence is returned as the markdown body.
// internal/frontmatter/frontmatter.go#L17-L35
func Split(content string) (map[string]string, string) {
// Looks for ^---\n, extracts until next --- or end
// Returns empty map + full content if no fence found
}
If no fence exists, the function returns an empty map and treats the entire file as body. This backward-compatible behavior allows legacy markdown files to load without errors.
Stage 2: YAML Decoding and Normalization
The parseYAMLFrontmatter function unmarshals YAML using gopkg.in/yaml.v3, then walks the node tree to produce a flat map[string]string:
| YAML Construct | How It's Flattened |
|---|---|
description: "value" |
Direct key-value: "description": "value" |
metadata:\n type: user |
Recursively flattened: "type": "user" |
allowed-tools: [read, write] |
Joined with , : "allowed-tools": "read, write" |
All keys are normalized via strings.ToLower, so Description: and description: are equivalent. Duplicate keys use "last write wins" resolution.
// Same file, parseYAMLFrontmatter implementation
// Uses addYAMLValue for recursive flattening
// Uses yamlScalarString for scalar extraction
Stage 3: Loader-Specific Field Extraction
Skills and commands each define their own expected key constants and parsing logic in dedicated packages.
Skill Frontmatter Parsing
Skills are loaded in internal/skill/skill.go. The Skill.Parse method (lines 68-99) calls splitFrontmatter, then consults skillFrontmatter* constants to populate a Skill struct.
Known Skill Keys
| Constant | Frontmatter Key | Parsed Value |
|---|---|---|
skillFrontmatterDescription |
description |
Direct string |
skillFrontmatterName |
name |
Falls back to file stem |
skillFrontmatterRunAs |
runas |
Processed by parseRunAs() |
skillFrontmatterModel |
model |
Direct string |
skillFrontmatterAllowedTools |
allowed-tools |
Split by parseAllowedTools() |
---
description: Summarise a web page
runas: subagent
model: sonnet
allowed-tools:
- read_file
- web_fetch
---
# Skill body
Use the web_fetch tool to retrieve the page and then summarise it.
// internal/skill/skill.go#L68-L99
content, _ := os.ReadFile("my-skill.md")
fm, body := frontmatter.Split(string(content))
skill := Skill{
Name: fm["name"], // or file stem
Description: fm["description"],
RunAs: parseRunAs(fm["runas"]),
Model: fm["model"],
AllowedTools: parseAllowedTools(fm["allowed-tools"]),
Body: body,
}
The parseAllowedTools function handles the comma-joined string (from YAML sequence) or a plain scalar, normalizing both into a slice.
Command Frontmatter Parsing
Commands follow an identical split-and-extract pattern in internal/command/command.go (lines 200-240), using commandFrontmatter* constants.
Known Command Keys
| Constant | Frontmatter Key | Purpose |
|---|---|---|
commandFrontmatterDescription |
description |
UI help text |
commandFrontmatterArguments |
arguments |
CLI parameter schema |
commandFrontmatterExample |
example |
Usage demonstration |
---
description: Print a greeting
arguments:
- name: who
type: string
description: Who to greet
example: hello --who Alice
---
Hello, {{.who}}!
// internal/command/command.go#L200-L240
fm, body := frontmatter.Split(string(content))
cmd := Command{
Name: "hello", // from filename
Description: fm["description"],
Arguments: parseArguments(fm["arguments"]), // parses YAML sequence
Example: fm["example"],
Body: body,
}
The parseArguments function deserializes the YAML sequence into typed Argument structs for CLI validation and UI rendering.
Strict Validation with frontmatter.Decode
For install-time integrity checks, Reasonix provides frontmatter.Decode (lines 37-44) with schema enforcement through struct tags:
type CmdMeta struct {
Description string `yaml:"description"`
Example string `yaml:"example"`
}
var meta CmdMeta
body, err := frontmatter.Decode(content, &meta, frontmatter.DecodeOptions{
KnownFields: true, // Reject unknown fields
})
// err != nil indicates malformed or incompatible frontmatter
The KnownFields: true option makes the parser strict, useful for internal/installsource/skill.go when vetting third-party contributions. Runtime loaders prefer the permissive Split for maximum compatibility.
Key Design Decisions
- Lower-case normalization: Eliminates case-sensitivity bugs across author conventions
- Flat string map: Keeps the core parser agnostic; semantic interpretation happens in domain packages
- Comma-join for sequences: Simplifies the common
allowed-toolsuse case while preserving readability - Graceful degradation: Missing frontmatter never blocks file loading
Summary
- Frontmatter parsing in Reasonix uses
frontmatter.Splitininternal/frontmatter/frontmatter.goto separate YAML metadata from markdown body - Three-stage pipeline: split on
---fences → flatten YAML tomap[string]stringwith lower-cased keys → domain-specific extraction - Skills parse
description,runas,model,allowed-tools, and other fields viainternal/skill/skill.go - Commands parse
description,arguments,example, and more viainternal/command/command.go - Strict validation available through
frontmatter.DecodewithKnownFields: truefor install-time checks
Frequently Asked Questions
How does Reasonix handle YAML frontmatter without strict schemas?
Reasonix uses the permissive Split function by default, which returns a flat map[string]string with all keys lower-cased. The skill and command loaders then check for specific keys they recognize, ignoring unknown fields. This allows files to include extra metadata for future use without breaking current parsing.
What happens if a skill or command file has no frontmatter?
The frontmatter.Split function returns an empty map and the full file content as body. Loaders detect missing fields and apply sensible defaults: the skill name falls back to the file stem, runas defaults to a safe value, and allowed-tools becomes an empty slice. Legacy plain-markdown files load without errors.
Can I use nested YAML structures in Reasonix frontmatter?
Yes, but with flattening. The addYAMLValue function recursively flattens nested mappings, so metadata:\n type: user becomes type: user at the top level. Sequences are converted to comma-separated strings. Deeply nested structures beyond this are flattened aggressively—design your frontmatter for the flat map model.
Why does Reasonix join array values with commas instead of preserving slices?
The map[string]string contract keeps the core parser simple and serializable. The comma-join convention ("read, write") covers the dominant use case (allowed-tools) while allowing loaders to split on demand. For complex structured data like command arguments, dedicated parse* functions reconstruct typed structs from the YAML source.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →