stolt/skill-validator
Parse and validate SKILL.md files (or raw content) against the SKILL.md format specification. Validates single files, entire directories (recursively), or existing SkillMd instances, returning a SkillMd on success or detailed errors on failure.
composer require stolt/skill-validator
use Stolt\Ai\Skill\Validator;
$validator = new Validator();
$result = $validator->validateFile(base_path('skills/example/SKILL.md'));
if ($result->isInvalid()) {
foreach ($result->errors() as $error) {
Log::error($error);
}
}
Add this to a Laravel app/Console/Commands/ValidateSkills.php:
public function handle() {
$validator = new Validator();
$results = $validator->validateFromDirectory(base_path('skills'));
foreach ($results as $path => $result) {
if ($result->isInvalid()) {
$this->error("Invalid skill at {$path}:");
foreach ($result->errors() as $error) {
$this->line(" - {$error}");
}
return 1;
}
}
$this->info('All skills validated successfully!');
}
Register the command in app/Console/Kernel.php and run with:
php artisan validate:skills
File Validation Pipeline:
// Single file
$result = $validator->validateFile($path);
// Directory scan (recursive)
$results = $validator->validateFromDirectory($directory);
Content Validation:
$rawContent = file_get_contents($path);
$result = $validator->validateContent($rawContent);
Programmatic Validation:
$skillMd = SkillMd::create('name', 'desc', '# Instructions', ['tags' => ['php']]);
$result = $validator->validateSkillMd($skillMd);
Service Provider Binding:
// app/Providers/AppServiceProvider.php
public function register() {
$this->app->singleton(Validator::class);
}
Request Validation Middleware:
// app/Http/Middleware/ValidateSkillUpload.php
public function handle(Request $request, Closure $next) {
$validator = app(Validator::class);
$result = $validator->validateContent($request->skill_content);
if ($result->isInvalid()) {
throw new \Exception('Invalid skill: ' . implode(', ', $result->errors()));
}
return $next($request);
}
Model Events:
// app/Models/Skill.php
protected static function booted() {
static::saved(function ($skill) {
$validator = new Validator();
$result = $validator->validateContent($skill->content);
if ($result->isInvalid()) {
throw new \Exception('Validation failed: ' . implode(', ', $result->errors()));
}
});
}
Validation Result Caching:
$cacheKey = 'skill_validation_' . md5($path);
$result = Cache::remember($cacheKey, now()->addHours(1), function() use ($validator, $path) {
return $validator->validateFile($path);
});
Skill Metadata Extraction:
$metadata = $result->metadata();
$tags = $metadata->get('tags', []);
$effort = $metadata->get('effort');
Round-Trip Validation:
$result = $validator->validateContent($rawContent);
$skillMd = $result->toSkillMd();
// Modify skillMd...
$skillMd->setDescription('Updated description');
// Re-validate
$revalidated = $validator->validateSkillMd($skillMd);
File Path Handling:
validateFromDirectory() uses absolute paths as keys in the result array. Normalize paths before use:
$results = $validator->validateFromDirectory($directory);
$relativePath = str_replace(base_path(), '', $filePath);
YAML Parsing Quirks:
tags: must be a list, not a string). Common errors:
# Invalid (string instead of list)
tags: php,laravel
# Valid
tags:
- php
- laravel
Name Validation:
code-review). Names like CodeReview or code_review will fail.Empty Markdown Body:
--- and frontmatter will fail.Boolean Fields:
disable-model-invocation must use YAML boolean syntax:
# Valid
disable-model-invocation: true
# Invalid (string)
disable-model-invocation: "true"
Inspect Raw Metadata:
$rawMetadata = $result->rawMetadata();
dump($rawMetadata); // Debug YAML parsing issues
Validate Incrementally:
validateSkillMd() to test individual SkillMd objects after modifications:
$skillMd = $result->toSkillMd();
$skillMd->setTags(['php', 'testing']);
$revalidated = $validator->validateSkillMd($skillMd);
Check for Hidden Characters:
SKILL.md files can cause parsing failures. Use:
$content = file_get_contents($path);
$content = preg_replace('/^\xEF\xBB\xBF/', '', $content); // Remove BOM
Custom Validation Rules:
use Stolt\Ai\Skill\Validator;
class CustomValidator extends Validator {
public function validateCustomRule($skillMd) {
if ($skillMd->name() === 'disallowed-skill') {
return ['Custom rule failed: Skill name disallowed'];
}
return null;
}
}
Override Default Rules:
stolt/skill-md for parsing. Override its behavior by replacing the dependency:
$validator = new Validator();
$validator->setSkillMdFactory(function() {
return new CustomSkillMd(); // Your implementation
});
Add Pre/Post-Validation Hooks:
$validator = new Validator();
$validator->setPreValidationCallback(function($input) {
if (is_string($input)) {
return str_replace('{{placeholder}}', 'value', $input);
}
return $input;
});
Directory Traversal:
validateFromDirectory() follows symlinks by default. Disable with:
$results = $validator->validateFromDirectory($directory, ['follow_symlinks' => false]);
Case Sensitivity:
Name: vs name:). Ensure YAML frontmatter uses lowercase keys.Performance:
SplFileInfo filtering to exclude non-SKILL.md files:
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($directory, \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::LEAVES_ONLY
);
$files = [];
foreach ($iterator as $file) {
if ($file->getExtension() === 'md' && $file->getBasename() === 'SKILL.md') {
$files[] = $file->getPathname();
}
}
$results = $validator->validateFiles($files);
Artisan Command Integration:
--path option for flexibility:
protected $signature = 'validate:skills {--path=}';
public function handle() {
$path = $this->option('path') ?? base_path('skills');
$validator = new Validator();
$results = $validator->validateFromDirectory($path);
// ...
}
API Response Formatting:
return response()->json([
'valid' => !$result->isInvalid(),
'errors' => $result->isInvalid() ? $result->errors() : [],
'metadata' => $result->metadata()?->toArray(),
]);
Storage Integration:
$skill = Skill::updateOrCreate
How can I help you explore Laravel packages today?