Files
2025-11-30 08:38:32 +08:00

269 lines
8.3 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Validate a Claude Code SKILL.md file.
Usage:
python validate-skill.py path/to/SKILL.md
"""
import re
import sys
from pathlib import Path
from typing import Tuple, List
# Validation rules
NAME_PATTERN = re.compile(r'^[a-z0-9-]+$')
MAX_NAME_LENGTH = 64
MAX_DESCRIPTION_LENGTH = 1024
def validate_yaml_frontmatter(content: str) -> Tuple[bool, List[str], dict]:
"""
Validate YAML frontmatter in SKILL.md.
Returns:
Tuple of (is_valid, errors, frontmatter_data)
"""
errors = []
frontmatter_data = {}
lines = content.split('\n')
# Check first line is ---
if not lines or lines[0].strip() != '---':
errors.append("YAML frontmatter must start with '---' on line 1")
return False, errors, frontmatter_data
# Find closing ---
closing_idx = None
for i, line in enumerate(lines[1:], start=1):
if line.strip() == '---':
closing_idx = i
break
if closing_idx is None:
errors.append("YAML frontmatter must end with '---'")
return False, errors, frontmatter_data
# Extract frontmatter
frontmatter_lines = lines[1:closing_idx]
# Parse YAML (simple parsing for validation)
for line in frontmatter_lines:
if ':' in line:
key, value = line.split(':', 1)
key = key.strip()
value = value.strip()
# Remove quotes if present
if value.startswith('"') and value.endswith('"'):
value = value[1:-1]
elif value.startswith("'") and value.endswith("'"):
value = value[1:-1]
frontmatter_data[key] = value
# Check required fields
if 'name' not in frontmatter_data:
errors.append("Missing required field: 'name'")
if 'description' not in frontmatter_data:
errors.append("Missing required field: 'description'")
return len(errors) == 0, errors, frontmatter_data
def validate_name(name: str) -> Tuple[bool, List[str]]:
"""Validate skill name."""
errors = []
if not name:
errors.append("Name cannot be empty")
return False, errors
if len(name) > MAX_NAME_LENGTH:
errors.append(f"Name exceeds maximum length of {MAX_NAME_LENGTH} characters (got {len(name)})")
if not NAME_PATTERN.match(name):
errors.append(f"Name must contain only lowercase letters, numbers, and hyphens (got: '{name}')")
# Provide specific feedback
if any(c.isupper() for c in name):
errors.append(" - Contains uppercase letters (use lowercase only)")
if '_' in name:
errors.append(" - Contains underscores (use hyphens instead)")
if ' ' in name:
errors.append(" - Contains spaces (use hyphens instead)")
if any(c in name for c in '!@#$%^&*()+=[]{}|\\;:\'",.<>?/'):
errors.append(" - Contains special characters (only hyphens allowed)")
return len(errors) == 0, errors
def validate_description(description: str) -> Tuple[bool, List[str]]:
"""Validate skill description."""
errors = []
warnings = []
if not description:
errors.append("Description cannot be empty")
return False, errors
if len(description) > MAX_DESCRIPTION_LENGTH:
errors.append(f"Description exceeds maximum length of {MAX_DESCRIPTION_LENGTH} characters (got {len(description)})")
# Check for best practices (warnings, not errors)
description_lower = description.lower()
if 'use when' not in description_lower:
warnings.append("Description should include 'Use when' to indicate when to activate this skill")
if len(description) < 50:
warnings.append("Description is quite short - consider adding more detail about what it does and when to use it")
# Check for vague terms
vague_terms = ['helps', 'assists', 'general', 'various', 'stuff', 'things']
found_vague = [term for term in vague_terms if term in description_lower]
if found_vague:
warnings.append(f"Description contains vague terms: {', '.join(found_vague)}. Be more specific.")
if warnings:
print("\n⚠️ Warnings (not errors, but consider addressing):")
for warning in warnings:
print(f" - {warning}")
return len(errors) == 0, errors
def validate_file_structure(skill_path: Path) -> Tuple[bool, List[str]]:
"""Validate the skill file structure."""
errors = []
warnings = []
skill_md = skill_path
skill_dir = skill_path.parent
if not skill_md.exists():
errors.append(f"SKILL.md not found at {skill_md}")
return False, errors
# Check for supporting files referenced in SKILL.md
content = skill_md.read_text()
# Find markdown links and code references
link_pattern = re.compile(r'\[([^\]]+)\]\(([^\)]+)\)')
matches = link_pattern.findall(content)
for link_text, link_path in matches:
# Skip external links
if link_path.startswith('http://') or link_path.startswith('https://'):
continue
# Check if referenced file exists
referenced_file = skill_dir / link_path
if not referenced_file.exists():
warnings.append(f"Referenced file not found: {link_path}")
if warnings:
print("\n⚠️ File Structure Warnings:")
for warning in warnings:
print(f" - {warning}")
return len(errors) == 0, errors
def validate_skill_file(skill_path: Path) -> bool:
"""
Validate a complete SKILL.md file.
Returns:
True if valid, False otherwise
"""
print(f"Validating: {skill_path}\n")
if not skill_path.exists():
print(f"❌ Error: File not found: {skill_path}")
return False
content = skill_path.read_text()
all_valid = True
# Validate YAML frontmatter
print("Checking YAML frontmatter...")
valid_yaml, yaml_errors, frontmatter = validate_yaml_frontmatter(content)
if not valid_yaml:
print("❌ YAML frontmatter errors:")
for error in yaml_errors:
print(f" - {error}")
all_valid = False
else:
print("✅ YAML frontmatter is valid")
# Validate name
if 'name' in frontmatter:
print("\nChecking name...")
valid_name, name_errors = validate_name(frontmatter['name'])
if not valid_name:
print("❌ Name validation errors:")
for error in name_errors:
print(f" - {error}")
all_valid = False
else:
print(f"✅ Name is valid: '{frontmatter['name']}'")
# Validate description
if 'description' in frontmatter:
print("\nChecking description...")
valid_desc, desc_errors = validate_description(frontmatter['description'])
if not valid_desc:
print("❌ Description validation errors:")
for error in desc_errors:
print(f" - {error}")
all_valid = False
else:
print(f"✅ Description is valid ({len(frontmatter['description'])} characters)")
# Validate allowed-tools if present
if 'allowed-tools' in frontmatter:
print("\nChecking allowed-tools...")
print(f"✅ Tool restrictions found: {frontmatter['allowed-tools']}")
# Validate file structure
print("\nChecking file structure...")
valid_structure, structure_errors = validate_file_structure(skill_path)
if not valid_structure:
print("❌ File structure errors:")
for error in structure_errors:
print(f" - {error}")
all_valid = False
else:
print("✅ File structure is valid")
# Summary
print("\n" + "="*60)
if all_valid:
print("✅ Skill validation PASSED")
print("\nYour skill is ready to use!")
print("Remember to restart Claude Code to load the new skill.")
else:
print("❌ Skill validation FAILED")
print("\nPlease fix the errors above and try again.")
print("="*60)
return all_valid
def main():
if len(sys.argv) != 2:
print("Usage: python validate-skill.py path/to/SKILL.md")
sys.exit(1)
skill_path = Path(sys.argv[1])
valid = validate_skill_file(skill_path)
sys.exit(0 if valid else 1)
if __name__ == '__main__':
main()