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 ID | Severity | Spec | What It Validates |
|---|
structure.skill-md-exists | ERROR | Yes | SKILL.md file exists |
structure.valid-frontmatter | ERROR | Yes | YAML frontmatter is parseable |
structure.standard-frontmatter-fields | WARNING | No | Only spec fields in frontmatter |
structure.scripts-valid | WARNING | No | Scripts folder contains valid files |
structure.references-valid | WARNING | No | References folder contains valid files |
structure.scripts-no-interactive | WARNING | No | Scripts avoid interactive input |
structure.scripts-self-contained | INFO | No | No 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
- Looks for
SKILL.md in the skill root directory
- If not found, checks for lowercase
skill.md variant
- Fails if lowercase variant exists (must be uppercase)
- 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
- Checks
skill.parse_errors for frontmatter-related errors
- Verifies that
skill.metadata was successfully parsed
- Fails if frontmatter has YAML syntax errors
- 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
- Passes if no
scripts/ folder exists (optional folder)
- Fails if
scripts exists but is not a directory
- Checks all files have valid script extensions:
.py, .sh, .js, .ts, .bash, .rb
- 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
- Passes if no
references/ folder exists (optional folder)
- Fails if
references exists but is not a directory
- Checks all files have valid reference extensions:
.md, .txt, .rst
- 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:
| Language | Patterns |
|---|
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:
| Manifest | Suggestion |
|---|
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 |
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.