Helper Library
claudelint provides utility functions for custom rules. All helpers are imported from claude-code-lint/utils.
Quick Start
import { hasHeading, extractFrontmatter } from 'claude-code-lint/utils';
export const rule: Rule = {
meta: { id: 'my-rule', /* ... */ },
validate: async (context) => {
const { frontmatter } = extractFrontmatter(context.fileContent);
if (!hasHeading(context.fileContent, 'Usage', 2)) {
context.report({ message: 'Missing ## Usage section' });
}
},
};Available Helpers
HeadingshasHeading, extractHeadingsCheck for specific headings and analyze document structure.
Pattern MatchingmatchesPattern, countOccurrences, findLinesMatchingSearch content with strings and regular expressions.
FrontmatterextractFrontmatter, validateSemverParse YAML frontmatter and validate version fields.
File SystemfileExists, readFileContentCheck file existence and read file content.
ParsingparseJSON, parseYAMLSafely parse JSON and YAML data formats.
Markdown UtilitiesextractBodyContent, stripCodeBlocks, extractImports, extractImportsWithLineNumbers, getFrontmatterFieldLine, countLinesManipulate markdown structure: extract body, strip fences, find imports.
Patterns and ConstantsescapeRegExp, containsEnvVar, isValidSemver, isImportPathRegex escaping, environment variable detection, and path classification.
Headings
hasHeading(content, text, level?)
Returns boolean. Check if markdown contains a specific heading. Pass an optional level (1-6) to match a specific heading depth.
if (!hasHeading(context.fileContent, 'Overview')) {
context.report({ message: 'Missing Overview section' });
}
if (!hasHeading(context.fileContent, 'Installation', 2)) {
context.report({ message: 'Missing ## Installation section' });
}extractHeadings(content)
Returns Array<{ text, level, line }>. Get all headings with their levels and line numbers.
const headings = extractHeadings(context.fileContent);
if (headings[0]?.level !== 1) {
context.report({
message: 'First heading must be level 1',
line: headings[0]?.line,
});
}Pattern Matching
matchesPattern(content, pattern)
Returns boolean. Test content against a regular expression.
if (matchesPattern(context.fileContent, /TODO:|FIXME:/i)) {
context.report({ message: 'Found TODO/FIXME comments' });
}countOccurrences(content, search)
Returns number. Count how many times a string or regex pattern appears.
const todoCount = countOccurrences(context.fileContent, /\bTODO\b/g);
if (todoCount > 0) {
context.report({ message: `Found ${todoCount} TODO comments` });
}findLinesMatching(content, pattern)
Returns Array<{ line, content }>. Find all matching lines with line numbers.
const matches = findLinesMatching(context.fileContent, /password\s*=/i);
matches.forEach(m => {
context.report({
message: 'Hardcoded password detected',
line: m.line,
});
});Frontmatter
extractFrontmatter(content)
Returns { frontmatter, content, hasFrontmatter }. Parse YAML frontmatter from markdown files.
const result = extractFrontmatter(context.fileContent);
if (!result.hasFrontmatter || !result.frontmatter) {
context.report({ message: 'Missing frontmatter' });
return;
}
if (!result.frontmatter.version) {
context.report({ message: 'Frontmatter missing version field' });
}validateSemver(version)
Returns boolean. Check if a string is a valid semantic version (e.g., 1.0.0, 2.1.3-beta).
if (fm?.version && !validateSemver(fm.version)) {
context.report({ message: `Invalid version format: ${fm.version}` });
}File System
fileExists(filePath)
Returns Promise<boolean>. Check if a file exists.
if (!(await fileExists('./README.md'))) {
context.report({ message: 'README.md not found' });
}readFileContent(filePath)
Returns Promise<string | null>. Read file content. Returns null on failure.
const content = await readFileContent('./config.json');
if (content === null) {
context.report({ message: 'Failed to read config.json' });
return;
}
const config = parseJSON(content);Parsing
parseJSON(content)
Returns object | null. Safely parse JSON content.
const data = parseJSON(context.fileContent);
if (!data) {
context.report({ message: 'Invalid JSON' });
return;
}parseYAML(content)
Returns object | null. Safely parse YAML content.
const data = parseYAML(context.fileContent);
if (!data) {
context.report({ message: 'Invalid YAML' });
return;
}Markdown Utilities
extractBodyContent(content)
Returns string. Strip frontmatter and return only the markdown body.
const body = extractBodyContent(context.fileContent);stripCodeBlocks(content)
Returns string. Remove all fenced code blocks (backtick and tilde) from content.
const prose = stripCodeBlocks(context.fileContent);
if (matchesPattern(prose, /TODO:/)) {
context.report({ message: 'TODO found outside code blocks' });
}extractImports(content)
Returns string[]. Get all @path import references from markdown content.
const imports = extractImports(context.fileContent);extractImportsWithLineNumbers(content)
Returns Array<{ path, line }>. Like extractImports but includes line numbers.
const imports = extractImportsWithLineNumbers(context.fileContent);
imports.forEach(imp => {
context.report({ message: `Import: ${imp.path}`, line: imp.line });
});getFrontmatterFieldLine(content, field)
Returns number | undefined. Find the line number of a specific frontmatter field.
const line = getFrontmatterFieldLine(context.fileContent, 'version');
if (line) {
context.report({ message: 'Invalid version', line });
}countLines(content)
Returns number. Count the number of lines in a string.
const lines = countLines(context.fileContent);
if (lines > 500) {
context.report({ message: 'File too long' });
}Patterns and Constants
escapeRegExp(str)
Returns string. Escape special regex characters in a string for use in new RegExp().
const pattern = new RegExp(escapeRegExp(userInput), 'g');containsEnvVar(str)
Returns boolean. Check if a string contains environment variable syntax ($VAR, ${VAR}, %VAR%).
if (containsEnvVar(url)) {
return; // Skip validation — URL uses env vars
}isValidSemver(str)
Returns boolean. Check if a string matches semver format (e.g., 1.0.0, 2.1.3-beta.1).
if (!isValidSemver(version)) {
context.report({ message: 'Invalid semver' });
}isImportPath(str)
Returns boolean. Check if an @-prefixed string is an import path (contains / or file extension) vs. a decorator or email.
if (isImportPath(match)) {
// It's an @import reference like @.claude/rules/git.md
}See Also
- Custom Rules Guide - Full working examples using these helpers
- Custom Rules Troubleshooting - Common issues and solutions