
Skill Validator
- 15 installs
- 1 repo stars
- Updated January 3, 2026
- skillscatalog/registry
skill-validator is an agent skill that validates Agent Skills folders against the official specification using a bundled Python CLI.
About
skill-validator is a Skill Development checker for solo and indie builders who ship agent skills to registries like Prism or skills.sh. It runs a Python CLI against a skill folder and applies the same specification rules the catalog uses, so you catch frontmatter, structure, and compliance issues before users install your package. Use it when you are authoring or updating SKILL.md, bundling scripts, or preparing a release that must pass manifest and integrity expectations. The skill bundles validate_skill.py with argparse-driven usage, JSON reporting for automation, and a strict mode for tighter gates. It fits the Build phase on the agent-tooling shelf and also supports Ship-phase review when you treat spec compliance as a launch checklist. You get repeatable validation instead of guessing whether an agent runtime will accept your skill layout.
- Mirrors catalog spec-compliance rules via validate_skill.py for consistent pass/fail behavior
- CLI validates a skill directory path with optional --json and --strict modes
- SHA-256 manifest integrity block documents SKILL.md and bundled script fingerprints
- Two-file layout: SKILL.md manifest plus scripts/validate_skill.py validator
- Structured validation output suitable for CI and pre-publish gates
Skill Validator by the numbers
- 15 all-time installs (skills.sh)
- Ranked #482 of 782 Skill Development skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 27, 2026 (Skillselion catalog sync)
npx skills add https://github.com/skillscatalog/registry --skill skill-validatorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 15 |
|---|---|
| repo stars | ★ 1 |
| Security audit | 3 / 3 scanners passed |
| Last updated | January 3, 2026 |
| Repository | skillscatalog/registry ↗ |
What it does
Validate local SKILL.md packages against the Agent Skills spec before you publish or submit them to a catalog.
Who is it for?
skill authors and maintainers who want the same validation logic as the skills catalog before every commit or publish.
Skip if: Skip if you only consume third-party skills and never author SKILL.md packages—skip unless you are packaging your own skill.
When should I use this skill?
Before publishing, submitting to a catalog, or merging changes to a skill package that must match Agent Skills specification.
What you get
You receive structured pass/fail validation from validate_skill.py so you can fix spec issues before catalog ingest or CI release.
- Console or JSON validation report for the skill path
- Spec compliance pass/fail suitable for CI pipelines
By the numbers
- 2 bundled files: SKILL.md manifest and scripts/validate_skill.py
- Supports --json and --strict CLI flags per bundled usage docs
Files
Instructions
Use this skill to validate any Agent Skill directory against the specification before publishing. The validator checks:
- SKILL.md presence - Required manifest file exists
- Frontmatter format - Valid YAML frontmatter with required fields
- Name validation - 1-64 chars, lowercase, hyphens only, starts with letter
- Name-directory match - Name field matches the directory name
- Description validation - 1-1024 characters, non-empty
- Structure validation - Valid folder organization
When to Use
- Before submitting a skill for publication
- After making changes to SKILL.md
- To diagnose why a skill was rejected
- As part of CI/CD pipelines
How to Use
Validate a skill directory:
Validate the skill at /path/to/my-skillGet JSON output for CI integration:
python3 validate_skill.py /path/to/my-skill --jsonOutput
✓ Skill Validation Report
Skill: pdf-tools
Status: COMPLIANT (100/100)
Checks:
✓ SKILL.md exists
✓ Valid frontmatter
✓ Name format valid (pdf-tools)
✓ Name matches directory
✓ Description valid (45 chars)
✓ Structure valid
No issues found.Or if issues are found:
✗ Skill Validation Report
Skill: My-Skill
Status: NON-COMPLIANT (40/100)
Checks:
✓ SKILL.md exists
✓ Valid frontmatter
✗ Name format invalid
✗ Name doesn't match directory
✓ Description valid
✓ Structure valid
Issues:
- Name must be lowercase with hyphens only (got "My-Skill")
- Name 'My-Skill' doesn't match directory 'my-skill'Examples
Basic validation:
User: Validate the skill at ./document-skills/pdf
Agent: Running validation on ./document-skills/pdf...
✓ Skill is compliant (100/100)Fix validation errors:
User: Why is my skill failing validation?
Agent: Let me check... Running validator...
The name field uses underscores. Change "pdf_tools" to "pdf-tools".Limitations
- Does not validate skill functionality (only structure/format)
- Does not run safety scans (use skill-safety-scanner for that)
- Requires local filesystem access to the skill directory
Dependencies
- Python 3.9+
- PyYAML (optional, falls back to basic parsing)
{
"$schema": "https://agentskills.io/schemas/manifest.v1.json",
"manifestVersion": "1.0",
"generatedAt": "2026-01-03T02:24:46.094996Z",
"generator": "skill-manifest-generator/1.0.0",
"skill": {
"name": "skill-validator",
"version": "1.0.0"
},
"integrity": {
"algorithm": "sha256",
"hash": "2c48316df8cb9231cc24b1387e24ef96b1ff5d383af74caea719185841c2bf6f"
},
"files": [
{
"path": "SKILL.md",
"size": 2586,
"sha256": "c662b1e1bc6846aeefe7cf06ad3be26518f2168bd4db68dfd38b9cb95bceabfe",
"type": "manifest"
},
{
"path": "scripts/validate_skill.py",
"size": 14502,
"sha256": "ebf57274d07bf593eab81cbf7b3455e406827960d0eec4e91831838411e39009",
"type": "script"
}
],
"externalReferences": [],
"structure": {
"maxDepth": 1,
"totalFiles": 2,
"totalBytes": 17088,
"folders": [
"scripts"
]
},
"license": {
"spdxId": "MIT"
}
}
#!/usr/bin/env python3
"""
Skill Validator
Validates Agent Skills against the specification.
This script mirrors the validation logic from src/lib/spec-compliance/index.ts
to ensure consistent behavior between the skill and the catalog.
Usage:
python validate_skill.py /path/to/skill
python validate_skill.py /path/to/skill --json
python validate_skill.py /path/to/skill --strict
"""
import argparse
import json
import os
import re
import sys
from dataclasses import dataclass, field, asdict
from pathlib import Path
from typing import Optional
# Try to import yaml, fall back to basic parsing if not available
try:
import yaml
HAS_YAML = True
except ImportError:
HAS_YAML = False
@dataclass
class ValidationResult:
"""Result of skill validation - mirrors ComplianceResult from TypeScript"""
is_compliant: bool
score: int # 0-100
has_skill_md: bool
has_valid_name: bool
has_valid_description: bool
name_matches_dir: bool
has_valid_structure: bool
skill_name: Optional[str]
description: Optional[str]
issues: list = field(default_factory=list)
warnings: list = field(default_factory=list)
# Additional fields for enhanced validation
version: Optional[str] = None
license: Optional[str] = None
author: Optional[str] = None
tags: list = field(default_factory=list)
def validate_name(name: str) -> tuple[bool, Optional[str]]:
"""
Validate a skill name according to spec.
Rules (from src/lib/spec-compliance/index.ts):
- 1-64 characters
- lowercase alphanumeric and hyphens only
- cannot start with hyphen
- cannot end with hyphen
- no consecutive hyphens
Extended rules (from specs/20-skill-manifest-system.md):
- must start with a letter
"""
if not name or len(name) == 0:
return False, "Name is required"
if len(name) > 64:
return False, "Name exceeds 64 characters"
# Must be lowercase alphanumeric and hyphens only
if not re.match(r'^[a-z0-9-]+$', name):
return False, "Name must be lowercase alphanumeric and hyphens only"
# Must start with a letter (extended rule from Spec 20)
if not re.match(r'^[a-z]', name):
return False, "Name must start with a letter"
if name.startswith("-"):
return False, "Name cannot start with a hyphen"
if name.endswith("-"):
return False, "Name cannot end with a hyphen"
if "--" in name:
return False, "Name cannot contain consecutive hyphens"
return True, None
def validate_description(desc: str) -> tuple[bool, Optional[str]]:
"""
Validate description according to spec.
Rules:
- 1-1024 characters
- non-empty
"""
if not desc or len(desc.strip()) == 0:
return False, "Description is required"
if len(desc) > 1024:
return False, "Description exceeds 1024 characters"
return True, None
def validate_version(version: str) -> tuple[bool, Optional[str]]:
"""Validate version follows semver format (warning only)."""
if not version:
return True, None # Version is optional
# Basic semver pattern
semver_pattern = r'^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$'
if not re.match(semver_pattern, version):
return False, f"Version '{version}' should follow semver format (e.g., 1.0.0)"
return True, None
def parse_frontmatter(content: str) -> Optional[dict]:
"""
Parse YAML frontmatter from SKILL.md content.
Mirrors parseFrontmatter from TypeScript.
"""
# Check for frontmatter delimiters
if not content.startswith("---"):
return None
# Find closing delimiter
end_match = re.search(r'\n---\s*\n', content[3:])
if not end_match:
# Try with just --- at end of line
end_match = re.search(r'\n---\s*$', content[3:])
if not end_match:
return None
frontmatter_str = content[3:3 + end_match.start()].strip()
if HAS_YAML:
try:
return yaml.safe_load(frontmatter_str) or {}
except yaml.YAMLError:
return None
else:
# Basic parsing without yaml library
result = {}
current_key = None
for line in frontmatter_str.split("\n"):
# Skip empty lines and comments
if not line.strip() or line.strip().startswith("#"):
continue
# Handle array items
if line.strip().startswith("- ") and current_key:
if current_key not in result:
result[current_key] = []
if isinstance(result[current_key], list):
result[current_key].append(line.strip()[2:].strip())
continue
# Handle key: value pairs
if ":" in line and not line.startswith(" "):
key, value = line.split(":", 1)
key = key.strip()
value = value.strip().strip('"').strip("'")
current_key = key
if value:
result[key] = value
else:
result[key] = [] # Prepare for array
return result
def get_folder_depth(skill_path: Path) -> int:
"""Calculate the maximum folder depth in the skill."""
max_depth = 0
for root, dirs, files in os.walk(skill_path):
# Skip hidden directories
dirs[:] = [d for d in dirs if not d.startswith('.')]
rel_path = Path(root).relative_to(skill_path)
depth = len(rel_path.parts)
max_depth = max(max_depth, depth)
return max_depth
def check_structure(skill_path: Path) -> tuple[bool, list]:
"""
Check if skill has valid structure.
Valid structure:
- SKILL.md at root
- Optional: scripts/, references/, assets/ directories
- Max depth of 6 levels (warning at 4+)
"""
issues = []
warnings = []
# Check for non-standard top-level directories
valid_dirs = {"scripts", "references", "assets", ".git", "__pycache__", "node_modules"}
for item in skill_path.iterdir():
if item.is_dir() and not item.name.startswith("."):
if item.name.lower() not in valid_dirs:
warnings.append(f"Non-standard directory: {item.name}")
# Check depth
depth = get_folder_depth(skill_path)
if depth > 6:
issues.append(f"Folder depth ({depth}) exceeds maximum of 6")
elif depth >= 4:
warnings.append(f"Deep nesting ({depth} levels) may be hard to maintain")
return len(issues) == 0, issues + warnings
def validate_skill(skill_path: Path, strict: bool = False) -> ValidationResult:
"""
Run complete validation on a skill directory.
Mirrors checkCompliance from TypeScript.
"""
issues = []
warnings = []
# Get directory name
dir_name = skill_path.name
# Check 1: SKILL.md exists
skill_md_path = skill_path / "SKILL.md"
has_skill_md = skill_md_path.exists()
if not has_skill_md:
# Also check for case-insensitive match
for item in skill_path.iterdir():
if item.name.lower() == "skill.md":
skill_md_path = item
has_skill_md = True
if item.name != "SKILL.md":
warnings.append(f"SKILL.md should be uppercase (found: {item.name})")
break
if not has_skill_md:
issues.append("Missing required SKILL.md file")
# Parse frontmatter if SKILL.md exists
frontmatter = None
if has_skill_md:
try:
content = skill_md_path.read_text(encoding="utf-8")
frontmatter = parse_frontmatter(content)
if frontmatter is None:
issues.append("SKILL.md missing YAML frontmatter (must start with ---)")
except Exception as e:
issues.append(f"Could not read SKILL.md: {e}")
# Check 2: Valid name
has_valid_name = False
skill_name = None
if frontmatter:
skill_name = frontmatter.get("name")
if skill_name:
valid, reason = validate_name(skill_name)
has_valid_name = valid
if not valid:
issues.append(f"Invalid name: {reason}")
else:
issues.append("Missing 'name' field in frontmatter")
# Check 3: Valid description
has_valid_description = False
description = None
if frontmatter:
description = frontmatter.get("description")
if description:
valid, reason = validate_description(description)
has_valid_description = valid
if not valid:
issues.append(f"Invalid description: {reason}")
else:
issues.append("Missing 'description' field in frontmatter")
# Check 4: Name matches directory
name_matches_dir = skill_name == dir_name if skill_name else False
if skill_name and not name_matches_dir:
issues.append(f"Name '{skill_name}' does not match directory name '{dir_name}'")
# Check 5: Valid structure
has_valid_structure, structure_issues = check_structure(skill_path)
for issue in structure_issues:
if "exceeds" in issue.lower():
issues.append(issue)
else:
warnings.append(issue)
# Optional field validation (warnings only, unless strict mode)
version = frontmatter.get("version") if frontmatter else None
if version:
valid, reason = validate_version(version)
if not valid:
if strict:
issues.append(reason)
else:
warnings.append(reason)
license_id = frontmatter.get("license") if frontmatter else None
author = frontmatter.get("author") if frontmatter else None
tags = frontmatter.get("tags", []) if frontmatter else []
# Validate tags
if tags:
if len(tags) > 10:
warnings.append(f"Too many tags ({len(tags)}), maximum is 10")
for tag in tags:
if not re.match(r'^[a-z0-9-]+$', str(tag)):
warnings.append(f"Tag '{tag}' should be lowercase with hyphens only")
# Calculate score (each check is worth 20 points)
checks = [
has_skill_md,
has_valid_name,
has_valid_description,
name_matches_dir,
has_valid_structure,
]
passed_checks = sum(1 for c in checks if c)
score = round((passed_checks / len(checks)) * 100)
# Overall compliance requires all critical checks
is_compliant = has_skill_md and has_valid_name and has_valid_description and name_matches_dir
return ValidationResult(
is_compliant=is_compliant,
score=score,
has_skill_md=has_skill_md,
has_valid_name=has_valid_name,
has_valid_description=has_valid_description,
name_matches_dir=name_matches_dir,
has_valid_structure=has_valid_structure,
skill_name=skill_name,
description=description,
issues=issues,
warnings=warnings,
version=version,
license=license_id,
author=author,
tags=tags if isinstance(tags, list) else [],
)
def print_result(result: ValidationResult, skill_path: Path):
"""Print validation result in human-readable format."""
status_icon = "✓" if result.is_compliant else "✗"
status_text = "COMPLIANT" if result.is_compliant else "NON-COMPLIANT"
print(f"\n{status_icon} Skill Validation Report")
print(f" Path: {skill_path}")
print(f" Skill: {result.skill_name or 'Unknown'}")
print(f" Status: {status_text} ({result.score}/100)")
print(f"\n Checks:")
checks = [
("SKILL.md exists", result.has_skill_md),
("Valid frontmatter", result.has_skill_md and result.skill_name is not None),
(f"Name format valid ({result.skill_name})" if result.skill_name else "Name format", result.has_valid_name),
("Name matches directory", result.name_matches_dir),
(f"Description valid ({len(result.description or '')} chars)" if result.description else "Description valid", result.has_valid_description),
("Structure valid", result.has_valid_structure),
]
for label, passed in checks:
icon = "✓" if passed else "✗"
print(f" {icon} {label}")
if result.issues:
print(f"\n Issues ({len(result.issues)}):")
for issue in result.issues:
print(f" ✗ {issue}")
if result.warnings:
print(f"\n Warnings ({len(result.warnings)}):")
for warning in result.warnings:
print(f" ⚠ {warning}")
if not result.issues and not result.warnings:
print(f"\n No issues found.")
# Show optional metadata if present
if result.version or result.license or result.author:
print(f"\n Metadata:")
if result.version:
print(f" Version: {result.version}")
if result.license:
print(f" License: {result.license}")
if result.author:
print(f" Author: {result.author}")
if result.tags:
print(f" Tags: {', '.join(result.tags)}")
print()
def main():
parser = argparse.ArgumentParser(
description="Validate Agent Skills against the specification"
)
parser.add_argument(
"skill_path",
type=Path,
help="Path to the skill directory"
)
parser.add_argument(
"--json",
action="store_true",
help="Output results as JSON"
)
parser.add_argument(
"--strict",
action="store_true",
help="Treat warnings as errors"
)
parser.add_argument(
"--quiet", "-q",
action="store_true",
help="Only output errors (exit code indicates pass/fail)"
)
args = parser.parse_args()
skill_path = args.skill_path.resolve()
if not skill_path.is_dir():
print(f"Error: Not a directory: {skill_path}", file=sys.stderr)
sys.exit(1)
result = validate_skill(skill_path, strict=args.strict)
if args.json:
output = asdict(result)
output["skill_path"] = str(skill_path)
print(json.dumps(output, indent=2))
elif not args.quiet:
print_result(result, skill_path)
# Exit code: 0 = compliant, 1 = non-compliant
sys.exit(0 if result.is_compliant else 1)
if __name__ == "__main__":
main()
Related skills
How it compares
Use instead of hand-reading the spec or ad-hoc lint scripts that drift from catalog compliance rules.
FAQ
Who is skill-validator for?
Developers and small teams authoring Agent Skills who need spec compliance checks aligned with catalog ingest rules before publish.
When should I use skill-validator?
During Build agent-tooling when packaging SKILL.md, before Ship review as a release gate, and in CI when validating skill directories with --json output.
Is skill-validator safe to install?
Review the Security Audits panel on this Prism page and inspect scripts/validate_skill.py locally; the skill runs filesystem reads on paths you pass to the CLI.