Core Concepts
Architecture Overview
The framework consists of four main components:
Validator - Core validation engine that orchestrates rule execution
Rules - Individual validation rules that check specific patterns
Auto-discovery - System that finds and loads rules automatically
CLI - Command-line interface for running validations
Validation Flow
When Claude Code attempts to edit or write a file:
Claude Code triggers a Write or Edit operation
The PreToolUse hook intercepts the operation
The CLI receives the operation details via stdin
Rules are auto-discovered from .claude/rules/**
Only applicable rules run (based on shouldRun())
Each rule validates the content
If errors are found, the operation is blocked
Claude receives feedback and can try again
ValidationContext
Every validation rule receives a context object with information about the operation:
interface ValidationContext {
toolName: string; // 'Edit' or 'Write'
filePath: string; // Path to the file being modified
content: string; // New/current content
oldContent?: string; // Previous content (for Edit only)
operation: "edit" | "write";
}
Example Context
For an Edit operation:
{
"toolName": "Edit",
"filePath": "src/components/MyComponent.vue",
"content": "<template><UFormField>...</UFormField></template>",
"oldContent": "<template><UFormGroup>...</UFormGroup></template>",
"operation": "edit"
}
ValidationRule
A validation rule is an object with four properties:
interface ValidationRule {
name: string;
description: string;
shouldRun: (context: ValidationContext) => boolean;
validate: (context: ValidationContext) => Promise<string[]> | string[];
}
name
Unique identifier for the rule. Used in logs and hook names.
name: "nuxt-ui";
description
Human-readable description of what the rule validates.
description: "Validate Nuxt UI component usage and patterns";
shouldRun()
Determines if this rule should run for the given context. Use this to filter by file type, path, or other criteria.
shouldRun: (context) => {
// Only run on Vue files
return context.filePath.endsWith(".vue");
};
Common patterns:
// File extension
shouldRun: (context) => /\.(ts|js)x?$/.test(context.filePath);
// Specific directory
shouldRun: (context) => context.filePath.startsWith("src/components/");
// Always run
shouldRun: () => true;
// Multiple conditions
shouldRun: (context) => {
return context.filePath.endsWith(".vue") && context.operation === "edit";
};
validate()
The validation logic. Returns an array of error messages. Empty array means validation passed.
validate(context) {
const errors: string[] = [];
if (context.content.includes('bad-pattern')) {
errors.push(
`â Found bad pattern\n` +
` â Use good-pattern instead\n` +
` đ File: ${context.filePath}`
);
}
return errors;
}
Can be async:
async validate(context) {
const errors: string[] = [];
// Perform async operations
const result = await someAsyncCheck(context.content);
if (!result.valid) {
errors.push(result.error);
}
return errors;
}
Auto-discovery
Rules are automatically discovered from .claude/rules/**/*.{ts,js}:
.claude/rules/
âââ nuxt-ui.ts â
Discovered
âââ define-model.ts â
Discovered
âââ my-rule.ts â
Discovered
âââ subdirectory/
â âââ another-rule.ts â
Discovered
âââ utils.ts â
Discovered (if exports ValidationRule)
Discovery Process
Glob pattern finds all .ts and .js files
Test files (*.test.ts, *.spec.ts) are ignored
Each file is imported dynamically
Exports are checked for ValidationRule interface
Valid rules are registered automatically
Requirements
For a rule to be discovered, it must:
- Be in
.claude/rules/** - Have a
.tsor.jsextension - Export a
ValidationRuleobject - Match the
ValidationRuleinterface
Example of discoverable rule:
import { defineCodeRule } from "@syncrolabs/claude-code-validator";
export const myRule = defineCodeRule({
name: "my-rule",
description: "My validation rule",
shouldRun: () => true,
validate: () => [],
});
Hookable System
The validator uses hookable to provide lifecycle hooks:
const validator = defineCodeValidator();
const hooks = validator.getHooks();
// Before any validation
hooks.hook("validate:before", (context) => {
console.log("Starting validation for:", context.filePath);
});
// After all validation
hooks.hook("validate:after", (context, errors) => {
console.log(`Found ${errors.length} errors`);
});
// For specific rule
hooks.hook("validate:my-rule", (context) => {
console.log("Running my-rule");
});
Error Format
Errors should be formatted for clarity:
errors.push(
`â Brief description of the error\n` +
` â Suggested fix or replacement\n` +
` âšī¸ Additional helpful information\n` +
` đ File: ${context.filePath}`
);
Example output:
â ī¸ VALIDATION ERRORS:
â UFormGroup is outdated. Use UFormField instead.
â Use: UFormField
đ File: src/components/MyComponent.vue
â Invalid color "red" on <UButton>
â Use Nuxt UI v4 semantic color: "error"
âšī¸ Valid colors: primary, neutral, error, success, warning
đ File: src/components/MyButton.vue