Rule System
claudelint's rule system is modeled after ESLint. Rules are self-contained modules that export metadata and a validation function. This page covers rule implementation patterns, the rule registry, and how validators execute rules.
Rule Interface
Every rule implements the Rule interface:
// src/types/rule.ts
export interface Rule {
meta: RuleMetadata;
validate: (context: RuleContext) => Promise<void> | void;
}
export interface RuleMetadata {
id: RuleId;
name: string;
description: string;
category: RuleCategory;
severity: RuleSeverity; // 'off' | 'warn' | 'error'
fixable: boolean;
deprecated?: boolean | DeprecationInfo;
since: string;
schema?: z.ZodType; // Zod schema for rule options
defaultOptions?: Record<string, unknown>;
docs?: RuleDocumentation; // Auto-generates doc pages
}
export interface RuleContext {
filePath: string;
fileContent: string;
options: Record<string, unknown>;
report: (issue: RuleIssue) => void;
}
export interface RuleIssue {
message: string;
line?: number;
fix?: string;
autoFix?: AutoFix;
}Rule Implementation Patterns
claudelint supports two primary patterns for implementing rules.
Schema-Delegating Rules
The "thin wrapper" pattern — use when validating individual frontmatter fields that have Zod schema definitions.
Rules delegate to Zod schema validators instead of duplicating validation logic. This is the actual pattern from skill-allowed-tools.ts:
export const rule: Rule = {
meta: {
id: 'skill-allowed-tools',
name: 'Skill Allowed Tools Format',
description:
'Skill allowed-tools must be an array of tool names',
category: 'Skills',
severity: 'error',
fixable: false,
since: '0.2.0',
docs: { /* ... */ },
},
validate: (context: RuleContext) => {
const { data: frontmatter } = extractFrontmatter(context.fileContent);
if (!frontmatter?.['allowed-tools']) return;
// Delegate to schema validator
const result = SkillFrontmatterWithRefinements.safeParse(frontmatter);
if (!result.success) {
// Report issues with line numbers
}
},
};Categories using this pattern: Skills, Agents, Output Styles
Standalone Validation
Use when: Validating file-level properties, cross-references, or complex logic that doesn't fit in schemas.
This is the actual pattern from claude-md-size.ts:
export const rule: Rule = {
meta: {
id: 'claude-md-size',
name: 'CLAUDE.md File Size',
description: 'CLAUDE.md exceeds maximum file size limit',
category: 'CLAUDE.md',
severity: 'warn',
fixable: false,
since: '0.2.0',
schema: z.object({
maxSize: z.number().positive().int().optional(),
}),
defaultOptions: { maxSize: 40000 },
docs: { /* ... */ },
},
validate: async (context) => {
const maxSize = (context.options.maxSize as number) ?? 40000;
const fileSize = await getFileSize(context.filePath);
if (fileSize >= maxSize) {
context.report({
message: `File size (${fileSize} bytes) exceeds limit (${maxSize} bytes)`,
line: 1,
});
}
},
};Categories using this pattern: CLAUDE.md, MCP, Settings, Hooks
Choosing the Right Pattern
Use Schema-Delegating when:
- Validating a single frontmatter field
- Field has corresponding Zod schema definition
- Validation is format/type checking (length, regex, enum)
Use Standalone Validation when:
- Validating file-level properties (size, encoding)
- Checking cross-references (does imported file exist?)
- Detecting patterns across multiple fields
- Implementing complex logic that doesn't fit in schemas
Rule Registry
The Rule Registry is a centralized store for all validation rules. Rules self-register at module load time when their module is imported. The auto-generated src/rules/index.ts imports all rule modules, triggering registration.
See the Rules Reference for the full list of rules organized by category.
Validator Registry
The Validator Registry manages validator discovery and instantiation using a factory pattern. Validators self-register at module load, similar to rules:
// src/validators/skills.ts (end of file)
ValidatorRegistry.register(
{
id: 'skills',
name: 'Skills Validator',
description: 'Validates Claude Code skill files',
filePatterns: ['SKILL.md'],
enabled: true,
},
(options) => new SkillsValidator(options)
);Validator vs Rule Responsibilities
Validators
Validators are orchestrators. They:
- Find files matching patterns
- Read file content
- Parse files (JSON, YAML, Markdown)
- Execute rules via
executeRulesForCategory() - Aggregate and report results
- Handle operational messages only (e.g., "No files found")
Rules
Rules own all validation logic:
- ALL validation logic
- Individual field checks and cross-field validation
- File existence checks and body content validation
- Everything users might want to configure/disable
Operational Messages
These are the only non-configurable messages (not rules):
- "No skill directories found"
- "No agent directories found"
- "SKILL.md not found in skill directory"
- "File not found: {path}" (when user specifies --path)
These are discovery/parsing failures, not validation failures.
What MUST Be a Rule
If a user might disagree with the check or want to disable it, it MUST be a rule:
// .claudelintrc.json
{
"rules": {
"lsp-server-name-too-short": "off"
}
}This matches ESLint, Prettier, and all modern linting tools.
Custom Rules
Custom rules are loaded from .claudelint/rules/ and integrate seamlessly with built-in rules. The CustomRuleLoader discovers, validates, and registers custom rules alongside built-in ones.
See the Custom Rules Guide for complete documentation.
"Rules" vs .claude/rules/
claudelint uses the term "rule" to mean a validation check (e.g., skill-missing-version). This is different from Claude Code's .claude/rules/ directory, which stores modular behavioral instructions that Claude loads at session start (e.g., code style guidelines, testing conventions). claudelint validates those files but doesn't use them as rules.