odysseus-dev/odysseus · warning · HTTPException
Could not parse SKILL.md: {e}
Error message
Could not parse SKILL.md: {e} What it means
400 from POST /{skill_id}/markdown: services.memory.skill_format.Skill.from_markdown raised while parsing the submitted text. The endpoint saves nothing on parse failure — the existing SKILL.md is untouched. The detail embeds the parser's message, so the exact front-matter or structure defect is named in the response.
Source
Thrown at routes/skills_routes.py:1583
@router.post("/{skill_id}/markdown")
async def save_skill_markdown(request: Request, skill_id: str):
"""Replace SKILL.md with new raw content. Parses + validates first."""
from services.memory.skill_format import Skill
user = _owner(request)
body = await request.json()
new_content = body.get("markdown")
if not isinstance(new_content, str) or not new_content.strip():
raise HTTPException(400, "markdown is required")
skills = skills_manager.load(owner=user)
match = next((s for s in skills if s.get("name") == skill_id or s.get("id") == skill_id), None)
if not match:
raise HTTPException(404, "Skill not found")
_verify_owner(match, user)
try:
sk = Skill.from_markdown(new_content)
except Exception as e:
raise HTTPException(400, f"Could not parse SKILL.md: {e}")
# Never rename on save: a changed `name` in the markdown would move
# the skill dir (update_skill) and orphan the original id, so a later
# delete 404s (#1333). Pin to the stored name, like _apply_skill_md.
sk.name = match.get("name")
if not sk.owner:
sk.owner = match.get("owner") or user
ok = skills_manager.update_skill(match.get("name"), {
"name": sk.name,
"description": sk.description,
"version": sk.version,
"category": sk.category,
"tags": sk.tags,
"platforms": sk.platforms,
"requires_toolsets": sk.requires_toolsets,
"fallback_for_toolsets": sk.fallback_for_toolsets,
"status": sk.status,
"confidence": sk.confidence,
"source": sk.source,View on GitHub (pinned to f9235ebbf1)
Solutions
- Read the detail after 'Could not parse SKILL.md:' — it pinpoints the YAML/token error and position.
- Validate the front-matter locally with a YAML parser before POSTing.
- Ensure the file starts with a '---' line, has name/description keys, and closes the front-matter with a second '---'.
- Quote scalar values containing colons or special characters.
Example fix
# before
md = open("SKILL.md").read() # broken front-matter
client.post(f"/api/skills/{sid}/markdown", json={"markdown": md}) # 400
# after
import yaml
md = open("SKILL.md").read()
fm = md.split("---")[1]
yaml.safe_load(fm) # raises locally with a clear error before the request
assert {"name", "description"} <= set(yaml.safe_load(fm))
client.post(f"/api/skills/{sid}/markdown", json={"markdown": md}) Defensive patterns
Strategy: validation
Validate before calling
import yaml
parts = new_content.split("---", 2)
assert len(parts) >= 3 and parts[0].strip() == "", "file must start with a '---' front-matter block"
fm = yaml.safe_load(parts[1])
assert isinstance(fm, dict) and "name" in fm and "description" in fm, "front-matter needs name and description" Type guard
def parses_as_skill_md(text: str) -> bool:
"""Cheap client-side gate mirroring Skill.from_markdown."""
try:
parts = text.split("---", 2)
if len(parts) < 3 or parts[0].strip():
return False
fm = yaml.safe_load(parts[1])
return isinstance(fm, dict) and bool(fm.get("name")) and bool(fm.get("description"))
except yaml.YAMLError:
return False Prevention
- Lint the front-matter locally with a YAML parser before every save.
- Use plain ASCII '---' delimiters, exactly two, with the block at byte 0.
- Quote values containing colons; avoid tabs for indentation.
- Surface the server detail verbatim in the editor — it names the exact parse error.
When it happens
Trigger: Saving markdown whose YAML front-matter is malformed (bad indent, unclosed quotes, tabs), missing required front-matter keys (name/description), missing the '---' delimiters entirely, or containing YAML types the parser rejects (unquoted colons, duplicate keys).
Common situations: Hand-editing SKILL.md and breaking YAML syntax; AI-generated markdown with mangled front-matter delimiters; pasting content that uses '···' or smart dashes instead of plain '---'.
Related errors
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/d1803473e172a5b0.
Report an issue: GitHub.