zylon-ai/private-gpt · error · SkillDomainError

MISSING_SKILL_MD

MISSING_SKILL_MD

Error message

SKILL.md is required

What it means

_extract_skill_md scans the stored files of a skill bundle for an entry whose path is exactly 'SKILL.md' and decodes it; when no file with that exact relative path exists, it raises SkillDomainError MISSING_SKILL_MD. Every valid skill bundle must contain SKILL.md at its root — it is the manifest that carries name/description frontmatter and instructions.

Source

Thrown at private_gpt/components/skills/services/skill_service.py:320

        self._cache.set(
            "skill-body",
            cache_key,
            parsed.body,
            ttl_seconds=_SKILL_BODY_CACHE_TTL_SECONDS,
        )
        return parsed.body

    async def list_version_files(self, version: SkillVersionEntity) -> list[str]:
        """Relative paths of all files bundled in a skill version."""
        files = await self._storage_component.list_files(version.storage_prefix)
        return sorted(files)


def _extract_skill_md(files: list[StoredFile]) -> str:
    for file in files:
        if file.path == "SKILL.md":
            return file.content.decode("utf-8")
    raise SkillDomainError(SkillErrorCode.MISSING_SKILL_MD, "SKILL.md is required")

View on GitHub (pinned to 4a030776a3)

Solutions

  1. Ensure the bundle contains SKILL.md at the root, exact casing, no directory prefix.
  2. When zipping, archive from inside the skill directory so stored paths are relative ('SKILL.md', not 'my-skill/SKILL.md').
  3. If your storage normalizes case, check for 'skill.md' and rename before packaging.
  4. Validate the bundle client-side before upload: `any(f.path == 'SKILL.md' for f in files)`.

Example fix

# before
# zip built from parent dir -> entries: ['my-skill/SKILL.md', ...]
await service.create_skill(..., files=files)  # MISSING_SKILL_MD

# after
# zip built inside the skill dir -> entries: ['SKILL.md', ...]
assert any(f.path == "SKILL.md" for f in files)
await service.create_skill(..., files=files)
Defensive patterns

Strategy: validation

Validate before calling

def bundle_has_skill_md(files: list) -> bool:
    return any(f.path == "SKILL.md" for f in files)

assert bundle_has_skill_md(files), "Bundle must contain SKILL.md at its root"

Type guard

def is_valid_skill_bundle(files: list) -> bool:
    return any(getattr(f, "path", None) == "SKILL.md" for f in files)

Try / catch

try:
    await service.create_skill(..., files=files)
except SkillDomainError as e:
    if e.code is SkillErrorCode.MISSING_SKILL_MD:
        files = normalize_bundle_paths(files)  # strip directory prefixes, fix casing
        await service.create_skill(..., files=files)
    else:
        raise

Prevention

When it happens

Trigger: Uploading/activating a bundle where no file has path == 'SKILL.md' — e.g. it was renamed (skill.md, SKILL.MD), nested in a subdirectory ('my-skill/SKILL.md'), dropped during packaging, or the zip was created from the parent directory so all paths are prefixed.

Common situations: Zipping the parent folder instead of its contents (paths like 'my-skill/SKILL.md'); case mismatches on case-sensitive stores; build scripts that exclude the manifest; editing a bundle by hand and forgetting to re-add the file.

Related errors


AI-assisted analysis of zylon-ai/private-gpt@4a030776a3 (2026-08-15). Data as JSON: /api/errors/ab2b95ce46ffced2. Report an issue: GitHub.