Fix: ElizaOS SKILL.md Frontmatter Parse Failure on CRLF Line Endings
When developing autonomous AI agents with ElizaOS (formerly ai16z/eliza) using modular skill definitions (plugin-agent-skills), developers often encounter a baffling failure mode: custom skills placed inside the skills directory fail to load, loadSkillFromStorage silently returns null, or the runtime validator outputs MISSING_METADATA: SKILL.md frontmatter could not be parsed.
This issue occurs almost exclusively on Windows development environments, WSL mounts, or CI/CD pipelines where Git checkouts have converted line endings from standard Unix line feeds (LF, \n) to Windows carriage returns (CRLF, \r\n).
If your agent is also experiencing conversational schema breakdown where actions fail to fire, check our companion guide on Fixing ElizaOS Action Triggers Returning AI Model Responses.
Technical Root Cause Analysis: The LF-Only Regex Gate
In the official ElizaOS Agent Skills plugin (plugins/plugin-agent-skills/src/parser.ts), frontmatter extraction is implemented through a hardcoded regular expression:
The Vulnerable Source Code
// Location: plugins/plugin-agent-skills/src/parser.ts
export function parseFrontmatter(content: string): {
frontmatter: SkillMetadata | null;
body: string
} {
// ❌ VULNERABLE: LF-only line ending anchor (^---\n)
const match = content.match(/^---\n([\s\S]*?)\n---\n?/);
if (!match) {
// Silently fails on any file authored with CRLF (\r\n) line breaks
return { frontmatter: null, body: content };
}
try {
const rawYaml = match[1];
const frontmatter = yaml.load(rawYaml) as SkillMetadata;
const body = content.slice(match[0].length).trim();
return { frontmatter, body };
} catch (error) {
return { frontmatter: null, body: content };
}
}
The Failure Cascade
- CRLF Encoding Injection: When a file is created on Windows (Notepad, VS Code default Windows settings) or cloned via Git with
core.autocrlf = true, the text starts with the byte sequence2D 2D 2D 0D 0A(---\r\n). - Regex Mismatch: The regular expression
/^---\n/strictly expects the byte sequence2D 2D 2D 0A(---\n). Because\rprecedes\n, the regex pattern evaluation immediately yieldsnull. - Silent Downstream Degradation:
loadSkillFromStorage()receives{ frontmatter: null }and aborts skill hydration.validateSkillDirectory()throws a fatalMISSING_METADATAerror.- The ElizaOS agent boots up without throwing a crash stack trace, but none of the skill’s actions, memory providers, or evaluators are registered in
runtime.actions.
Permanent Remediation Protocol
To completely eliminate SKILL.md parsing errors across all operating systems and deployment pipelines, apply the following three-tier fix.
Step 1: Patch parser.ts with CRLF-Agnostic Regex
Modify the parser regex in plugins/plugin-agent-skills/src/parser.ts to explicitly permit optional \r carriage returns across all frontmatter delimiters:
// Location: plugins/plugin-agent-skills/src/parser.ts
import * as yaml from 'js-yaml';
export interface SkillMetadata {
name: string;
description: string;
version?: string;
author?: string;
tags?: string[];
[key: string]: any;
}
// ✅ ROBUST REGEX: Supports both LF (\n) and CRLF (\r\n) boundaries
const FRONTMATTER_REGEX = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
export function parseFrontmatter(content: string): {
frontmatter: SkillMetadata | null;
body: string
} {
if (!content || typeof content !== 'string') {
return { frontmatter: null, body: '' };
}
// Pre-normalize or match directly with cross-platform regex
const match = content.match(FRONTMATTER_REGEX);
if (!match) {
console.warn('[plugin-agent-skills] Warning: Failed to extract YAML frontmatter header.');
return { frontmatter: null, body: content };
}
try {
const rawYaml = match[1];
const frontmatter = yaml.load(rawYaml) as SkillMetadata;
const body = content.slice(match[0].length).trim();
return { frontmatter, body };
} catch (error) {
console.error('[plugin-agent-skills] YAML parsing exception in SKILL.md:', error);
return { frontmatter: null, body: content };
}
}
Step 2: Enforce Repository-Level .gitattributes
Ensure that Git never converts Markdown or configuration files to Windows CRLF during checkout on developer machines or Windows CI runners:
Create or update .gitattributes in your repository root:
# Ensure all markdown, skill manifests, and JSON stay LF across all OS checkouts
*.md text eol=lf
*.json text eol=lf
*.ts text eol=lf
*.js text eol=lf
SKILL.md text eol=lf
After updating .gitattributes, normalize existing files in your repository:
git add --renormalize .
git commit -m "fix(skills): normalize repository line endings to LF"
Step 3: Configure VS Code Workspace Settings
Add .vscode/settings.json to prevent your local editor from saving new skill templates with Windows CRLF:
{
"files.eol": "\n",
"[markdown]": {
"files.eol": "\n",
"editor.trimAutoWhitespace": true
}
}
Batch Line Ending Conversion Script
If you have dozens of existing skill directories failing validation, run this Node.js utility script to convert all SKILL.md files in your workspace to clean LF formatting:
// scripts/sanitize-skills.js
const fs = require('fs');
const path = require('path');
function sanitizeSkillFiles(dir) {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
sanitizeSkillFiles(fullPath);
} else if (entry.name.toLowerCase() === 'skill.md') {
const original = fs.readFileSync(fullPath, 'utf8');
const normalized = original.replace(/\r\n/g, '\n');
if (original !== normalized) {
fs.writeFileSync(fullPath, normalized, 'utf8');
console.log(`[+] Normalized CRLF -> LF in: ${fullPath}`);
}
}
}
}
const skillsRoot = path.resolve(__dirname, '../skills');
if (fs.existsSync(skillsRoot)) {
sanitizeSkillFiles(skillsRoot);
console.log('Skill directory sanitization complete.');
} else {
console.error(`Skills directory not found at: ${skillsRoot}`);
}
Execute via terminal:
node scripts/sanitize-skills.js
Diagnostic Matrix: Frontmatter Parsing Failures
| Symptom / Log Output | Underlying Bug | Solution |
|---|---|---|
MISSING_METADATA: SKILL.md frontmatter could not be parsed | Windows \r\n line breaks in YAML frontmatter delimiters | Patch regex to /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/ |
loadSkillFromStorage returns null | Frontmatter regex returned null, triggering silent guard exit | Sanitize file with dos2unix SKILL.md |
| Agent actions not registering on runtime boot | Skill metadata failed validation during startup sequence | Verify YAML syntax with js-yaml validator |
YAMLException: bad indentation of a mapping entry | Tabs instead of spaces in YAML frontmatter block | Replace tabs with 2 spaces in YAML keys |
Frequently Asked Questions
Q: Why does the skill load successfully on Linux production servers but fail on my Windows local machine?
Linux git checkouts retain standard Unix LF (\n) line feeds, which perfectly match the original regex. Windows development environments default to CRLF (\r\n), causing the exact same repository to fail locally while running fine in Docker/Linux.
Q: Can I use standard npm packages like gray-matter instead of regex?
Yes! Replacing custom regex extraction with robust libraries like gray-matter or yaml-front-matter handles cross-platform delimiters, UTF-8 BOM headers, and multi-line scalar values automatically without manual regex patching.
Q: What is the minimal valid SKILL.md frontmatter structure in ElizaOS?
Every SKILL.md must include at least a name and description:
---
name: "defi-swap-skill"
description: "Executes decentralized token swaps across EVM DEX protocols"
version: "1.0.0"
---
# Skill Instructions and Action Implementations