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 from src/types/rule.ts:
// 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 src/rules/skills/skill-allowed-tools.ts:
// src/rules/skills/skill-allowed-tools.ts
import { Rule, RuleContext } from '../../types/rule';
import {
SkillFrontmatterSchema,
SkillFrontmatterWithRefinements,
} from '../../schemas/skill-frontmatter.schema';
import { extractFrontmatter, getFrontmatterFieldLine } from '../../utils/formats/markdown';
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, cannot be used with disallowed-tools',
category: 'Skills',
severity: 'error',
fixable: false,
deprecated: 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 src/rules/claude-md/claude-md-size.ts:
// src/rules/claude-md/claude-md-size.ts
import { Rule } from '../../types/rule';
import { getFileSize } from '../../utils/filesystem/files';
import { z } from 'zod';
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,
deprecated: false,
since: '0.2.0',
schema: z.object({
maxSize: z.number().positive().int().optional(),
}),
defaultOptions: {
maxSize: 40000, // 40KB
},
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
Location: src/utils/rules/registry.ts
The Rule Registry is a centralized store for all validation rules. Rules self-register at module load time.
// src/utils/rules/registry.ts
export class RuleRegistry {
private static rules = new Map<RuleId, Rule>();
private static categoryCache = new Map<RuleCategory, Rule[]>();
static register(rule: Rule | RuleMetadata): void;
static get(ruleId: string): RuleMetadata | undefined;
static getRule(ruleId: string): Rule | undefined;
static getAll(): RuleMetadata[];
static getAllRules(): Rule[];
static getRulesByCategory(category: RuleCategory): Rule[];
}Rules auto-register when their module is imported. The src/rules/index.ts file imports all rule modules, triggering registration:
// src/rules/index.ts (auto-generated)
import './claude-md/claude-md-size';
import './skills/skill-allowed-tools';
// ... all other rulesRegistered Rule Categories
- Skills (45 rules)
- CLAUDE.md (15 rules)
- MCP (11 rules)
- Agents (12 rules)
- Plugin (12 rules)
- LSP (6 rules)
- Output Styles (6 rules)
- Settings (5 rules)
- Hooks (3 rules)
- Commands (2 rules)
- Total: 117 rules
Validator Registry
Location: src/utils/validators/factory.ts
The Validator Registry manages validator discovery and instantiation using a factory pattern.
// src/utils/validators/factory.ts
export class ValidatorRegistry {
private static validators = new Map<string, ValidatorRegistration>();
static register(metadata: ValidatorMetadata, factory: ValidatorFactory): void;
static create(id: string, options?: BaseValidatorOptions): FileValidator;
static getAll(options?: BaseValidatorOptions): FileValidator[];
static getEnabled(options?: BaseValidatorOptions): FileValidator[];
static getAllMetadata(): ValidatorMetadata[];
}Validators self-register at module load:
// 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 (at src/utils/rules/loader.ts) discovers, validates, and registers custom rules alongside built-in ones.
See the Custom Rules Guide for complete documentation.