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
- Verify the archive: `unzip -l skill.zip` must show SKILL.md at the root or under a single wrapper directory.
- Re-zip from inside the skill directory so SKILL.md is at the archive root (`cd my-skill && zip -r ../skill.zip .`).
- If uploading loose files, add a separate part named SKILL.md.
- 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
- Always run `unzip -l` (or list entries in code) before uploading a skill archive.
- Automate packaging with a script that asserts SKILL.md presence and fails the build otherwise.
- Keep SKILL.md at the skill root in the repo so naive zips work.
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.