zylon-ai/private-gpt · error · SkillDomainError

MISSING_SKILL_MD

MISSING_SKILL_MD

Error message

Upload must include a SKILL.md file.

What it means

SkillDomainError with code MISSING_SKILL_MD: after all uploads are read (zips extracted, paths normalized), the pipeline requires a resolved file at exactly 'SKILL.md' as the skill manifest per the Agent Skills spec. If 'SKILL.md' is not among the resolved paths, this error is raised before storing the version.

Source

Thrown at private_gpt/server/skills/skills_files.py:98

            }

    for value in resolved.values():
        if not value.mime_type:
            mime_type, _ = (
                ("text/markdown", None)
                if value.path.endswith(".md")
                else mimetypes.guess_type(value.path)
            )
            value.mime_type = mime_type or default_mime_type

        if not value.mime_type:
            raise SkillDomainError(
                SkillErrorCode.MIME_TYPE_UNKNOWN,
                f"Could not determine MIME type for file: {value.path}",
            )

    if "SKILL.md" not in resolved:
        raise SkillDomainError(
            SkillErrorCode.MISSING_SKILL_MD, "Upload must include a SKILL.md file."
        )

    return list(resolved.values())


def _normalize_path(path: str) -> str:
    # Normalize backslashes to forward slashes
    path = path.replace("\\", "/")

    parsed = PurePosixPath(path)

    if parsed.is_absolute():
        raise SkillDomainError(
            SkillErrorCode.UNSAFE_PATH_ABSOLUTE,
            f"Unsafe file path (absolute): {path!r}",
        )

View on GitHub (pinned to 4a030776a3)

Solutions

  1. Verify the archive: `unzip -l skill.zip` must show SKILL.md at the root or under a single wrapper directory.
  2. Re-zip from inside the skill directory so SKILL.md is at the archive root (`cd my-skill && zip -r ../skill.zip .`).
  3. If uploading loose files, add a separate part named SKILL.md.
  4. Ensure the zip-flattening logic finds exactly one SKILL.md; restructure nested archives (zip-in-zip is not expanded).

Example fix

# before
cd skills-repo && zip -r my-skill.zip my-skill   # SKILL.md ends up under my-skill/ only if missing
# after
cd skills-repo/my-skill && zip -r ../my-skill.zip .  # SKILL.md at archive root
Defensive patterns

Strategy: validation

Validate before calling

import zipfile
from pathlib import PurePosixPath

def zip_has_skill_md(path: str) -> bool:
    with zipfile.ZipFile(path) as zf:
        return any(PurePosixPath(n).name.lower() == 'skill.md' for n in zf.namelist())

Type guard

const zipHasSkillMd = (names: string[]): boolean =>
  names.some((n) => n.split('/').pop()?.toLowerCase() === 'skill.md');

Try / catch

try {
  await uploadSkill(zipBlob);
} catch (e: any) {
  if (e?.code === 'MISSING_SKILL_MD') {
    zipBlob = await rebuildZipWithSkillMd(zipBlob);
    await uploadSkill(zipBlob);
  } else throw e;
}

Prevention

When it happens

Trigger: Uploading a zip that lacks SKILL.md anywhere; uploading loose files without a SKILL.md part; a zip whose SKILL.md sits in a wrapper directory that _flatten_wrapper_directory could not strip (e.g. multiple candidate roots or SKILL.md nested deeper than the detected root); case variants like 'skill.md' are normalized by _normalize_path, so genuine absence is the usual cause.

Common situations: Packaging the wrong directory (parent of the skill folder) into the zip; zipping generated artifacts without the manifest; CI building archives with a script that excludes *.md; collaborators renaming SKILL.md locally.

Related errors


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