Skip to main content
Structure checks validate the physical organization of skill files, including SKILL.md existence, frontmatter validity, and optional folder contents.

Check List

This dimension includes 7 checks (2 spec-required, 5 quality).
Check IDSeveritySpecWhat It Validates
structure.skill-md-existsERRORYesSKILL.md file exists
structure.valid-frontmatterERRORYesYAML frontmatter is parseable
structure.standard-frontmatter-fieldsWARNINGNoOnly spec fields in frontmatter
structure.scripts-validWARNINGNoScripts folder contains valid files
structure.references-validWARNINGNoReferences folder contains valid files
structure.scripts-no-interactiveWARNINGNoScripts avoid interactive input
structure.scripts-self-containedINFONoNo dependency manifests in scripts/

structure.skill-md-exists

Severity: ERROR | Spec Required: Yes Validates that the SKILL.md file exists in the skill directory.

What It Checks

  1. Looks for SKILL.md in the skill root directory
  2. If not found, checks for lowercase skill.md variant
  3. Fails if lowercase variant exists (must be uppercase)
  4. Fails if neither exists

Implementation

def run(self, skill: Skill) -> CheckResult:
    skill_md_path = skill.path / "SKILL.md"

    if skill_md_path.exists():
        return self._pass(
            "SKILL.md found",
            location=str(skill_md_path),
        )

    # Check for lowercase variant
    skill_md_lower = skill.path / "skill.md"
    if skill_md_lower.exists():
        return self._fail(
            "SKILL.md should be uppercase (found skill.md)",
            location=str(skill_md_lower),
        )

    return self._fail(
        "SKILL.md file not found",
        location=str(skill.path),
    )
The file name must be exactly SKILL.md (uppercase). A lowercase skill.md file will be rejected.

structure.valid-frontmatter

Severity: ERROR | Spec Required: Yes Validates that YAML frontmatter at the top of SKILL.md is parseable and valid.

What It Checks

  1. Checks skill.parse_errors for frontmatter-related errors
  2. Verifies that skill.metadata was successfully parsed
  3. Fails if frontmatter has YAML syntax errors
  4. Fails if no frontmatter is present

Implementation

def run(self, skill: Skill) -> CheckResult:
    # Check for parse errors related to frontmatter
    frontmatter_errors = [e for e in skill.parse_errors if "frontmatter" in e.lower()]

    if frontmatter_errors:
        return self._fail(
            "Invalid YAML frontmatter",
            details={"errors": frontmatter_errors},
            location=self._skill_md_location(skill),
        )

    if skill.metadata is None:
        return self._fail(
            "No frontmatter found in SKILL.md",
            location=self._skill_md_location(skill),
        )

    return self._pass(
        "Valid YAML frontmatter",
        location=self._skill_md_location(skill),
    )
Frontmatter must be enclosed in --- delimiters at the start of the file.

structure.standard-frontmatter-fields

Severity: WARNING | Spec Required: No Verifies that frontmatter only contains fields defined in the Agent Skills specification.

What It Checks

Compares frontmatter fields against the official spec fields:
  • name (required)
  • description (required)
  • license (optional)
  • compatibility (optional)
  • metadata (optional)
  • allowed-tools (optional/experimental)
Fails if any non-standard fields are found.

Implementation

SPEC_FRONTMATTER_FIELDS = {
    "name",
    "description",
    "license",
    "compatibility",
    "metadata",
    "allowed-tools",
}

def run(self, skill: Skill) -> CheckResult:
    if skill.metadata is None:
        return self._pass(
            "No frontmatter to check",
            location=self._skill_md_location(skill),
        )

    raw_fields = set(skill.metadata.raw.keys())
    non_standard = raw_fields - SPEC_FRONTMATTER_FIELDS

    if non_standard:
        sorted_fields = sorted(non_standard)
        return self._fail(
            f"Non-standard frontmatter fields: {', '.join(sorted_fields)}. "
            "Move custom fields to the metadata map",
            details={
                "non_standard_fields": sorted_fields,
                "spec_fields": sorted(SPEC_FRONTMATTER_FIELDS),
                "note": "Custom fields should be placed in the metadata map instead",
            },
            location=self._skill_md_location(skill),
        )

    return self._pass(
        "All frontmatter fields are spec-compliant",
        location=self._skill_md_location(skill),
    )

Example

# ❌ Bad - custom field at top level
---
name: my-skill
description: Does something
author: me
---

# ✅ Good - custom field in metadata
---
name: my-skill
description: Does something
metadata:
  author: me
---

structure.scripts-valid

Severity: WARNING | Spec Required: No Validates that the optional /scripts folder contains only valid script file types.

What It Checks

  1. Passes if no scripts/ folder exists (optional folder)
  2. Fails if scripts exists but is not a directory
  3. Checks all files have valid script extensions: .py, .sh, .js, .ts, .bash, .rb
  4. Fails if any invalid file extensions are found

Valid Extensions

VALID_SCRIPT_EXTENSIONS = {".py", ".sh", ".js", ".ts", ".bash", ".rb"}

Implementation

def run(self, skill: Skill) -> CheckResult:
    scripts_path = skill.path / "scripts"

    if not scripts_path.exists():
        return self._pass(
            "No scripts folder present (optional)",
        )

    if not scripts_path.is_dir():
        return self._fail(
            "scripts is not a directory",
            location=str(scripts_path),
        )

    invalid_files: list[str] = []
    for item in scripts_path.iterdir():
        if item.is_file() and item.suffix.lower() not in VALID_SCRIPT_EXTENSIONS:
            invalid_files.append(item.name)

    if invalid_files:
        return self._fail(
            f"Scripts folder contains invalid files: {', '.join(invalid_files)}",
            details={
                "invalid_files": invalid_files,
                "valid_extensions": list(VALID_SCRIPT_EXTENSIONS),
            },
            location=str(scripts_path),
        )

    return self._pass(
        "Scripts folder contains only valid script files",
        location=str(scripts_path),
    )

structure.references-valid

Severity: WARNING | Spec Required: No Validates that the optional /references folder contains only valid reference document types.

What It Checks

  1. Passes if no references/ folder exists (optional folder)
  2. Fails if references exists but is not a directory
  3. Checks all files have valid reference extensions: .md, .txt, .rst
  4. Fails if any invalid file extensions are found

Valid Extensions

VALID_REFERENCE_EXTENSIONS = {".md", ".txt", ".rst"}

Implementation

def run(self, skill: Skill) -> CheckResult:
    references_path = skill.path / "references"

    if not references_path.exists():
        return self._pass(
            "No references folder present (optional)",
        )

    if not references_path.is_dir():
        return self._fail(
            "references is not a directory",
            location=str(references_path),
        )

    invalid_files: list[str] = []
    for item in references_path.iterdir():
        if item.is_file() and item.suffix.lower() not in VALID_REFERENCE_EXTENSIONS:
            invalid_files.append(item.name)

    if invalid_files:
        return self._fail(
            f"References folder contains invalid files: {', '.join(invalid_files)}",
            details={
                "invalid_files": invalid_files,
                "valid_extensions": list(VALID_REFERENCE_EXTENSIONS),
            },
            location=str(references_path),
        )

    return self._pass(
        "References folder contains only valid reference files",
        location=str(references_path),
    )

structure.scripts-no-interactive

Severity: WARNING | Spec Required: No Checks that scripts do not use interactive input patterns, since agents run non-interactive shells.

What It Checks

Scans script files for language-specific interactive input patterns:
LanguagePatterns
Python (.py)input(, getpass.getpass(
Shell (.sh, .bash)read, select
Ruby (.rb)gets, STDIN.gets
JS/TS (.js, .ts)readline, prompt(, process.stdin

Implementation

_INTERACTIVE_PATTERNS: dict[frozenset[str], list[tuple[str, re.Pattern[str]]]] = {
    # Python: input() and getpass
    frozenset({".py"}): [
        ("input(", re.compile(r"^[^#]*\binput\s*\(")),
        ("getpass.getpass(", re.compile(r"^[^#]*\bgetpass\.getpass\s*\(")),
    ],
    # Shell: read and select builtins
    frozenset({".sh", ".bash"}): [
        ("read", re.compile(r"^[^#]*\bread\b")),
        ("select", re.compile(r"^[^#]*\bselect\b")),
    ],
    # Ruby: gets and STDIN.gets
    frozenset({".rb"}): [
        ("gets", re.compile(r"^[^#]*\bgets\b")),
        ("STDIN.gets", re.compile(r"^[^#]*\bSTDIN\.gets\b")),
    ],
    # JS/TS: readline, prompt(), process.stdin
    frozenset({".js", ".ts"}): [
        ("readline", re.compile(r"^[^/]*\breadline\b")),
        ("prompt(", re.compile(r"^[^/]*\bprompt\s*\(")),
        ("process.stdin", re.compile(r"^[^/]*\bprocess\.stdin\b")),
    ],
}
Scripts with interactive input will fail to execute properly in agent environments, which run non-interactive shells.

structure.scripts-self-contained

Severity: INFO | Spec Required: No Checks that the scripts/ folder has no loose dependency manifests that would prevent self-contained execution.

What It Checks

Looks for dependency manifest files in scripts/ and provides suggestions for self-contained alternatives:
ManifestSuggestion
requirements.txtUse inline script metadata (PEP 723) or pip install in the script
package.jsonUse npx or bundle dependencies inline
GemfileUse inline gem install or bundler inline
go.modUse go run with module-aware mode
deno.jsonUse URL imports instead of a config file

Implementation

_DEPENDENCY_MANIFESTS: dict[str, str] = {
    "requirements.txt": "Use inline script metadata (PEP 723) or pip install in the script",
    "package.json": "Use npx or bundle dependencies inline",
    "Gemfile": "Use inline gem install or bundler inline",
    "go.mod": "Use go run with module-aware mode",
    "deno.json": "Use URL imports instead of a config file",
}

def run(self, skill: Skill) -> CheckResult:
    scripts_path = skill.path / "scripts"

    if not scripts_path.exists() or not scripts_path.is_dir():
        return self._pass("No scripts folder present (optional)")

    found: dict[str, str] = {}
    for manifest, suggestion in _DEPENDENCY_MANIFESTS.items():
        if (scripts_path / manifest).exists():
            found[manifest] = suggestion

    if found:
        names = ", ".join(sorted(found))
        return self._fail(
            f"Scripts folder contains dependency manifests: {names}",
            details={
                "manifests": list(found.keys()),
                "suggestions": found,
            },
            location=str(scripts_path),
        )

    return self._pass(
        "Scripts folder is self-contained (no dependency manifests)",
        location=str(scripts_path),
    )

Example: Self-Contained Python Script

#!/usr/bin/env python3
# /// script
# dependencies = ["requests", "beautifulsoup4"]
# ///

import sys
import subprocess

# Install dependencies inline
subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", "requests", "beautifulsoup4"])

import requests
from bs4 import BeautifulSoup

# Script logic here...
This is an INFO-level check (suggestion only). Self-contained scripts improve portability but are not required.

Build docs developers (and LLMs) love