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

  1. Read the detail after 'Could not parse SKILL.md:' — it pinpoints the YAML/token error and position.
  2. Validate the front-matter locally with a YAML parser before POSTing.
  3. Ensure the file starts with a '---' line, has name/description keys, and closes the front-matter with a second '---'.
  4. 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

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.