git:20260219.4d3562c to git:20260222.b6c1512

5 added, 1 removed. Audit A to A.

# Core Package (NPM Linter)
## Commands
```bash
npm run build # Compile TypeScript to dist/
npm run dev # Watch mode (tsc --watch)
npm test # Full Jest suite
npm run test:watch # Jest watch mode
npm run test:validators # Validator tests with coverage
npm run test:schemas # Schema tests with coverage
npm run generate:types # Regenerate rule index and ID types
npm run check:self # Build then run claudelint on itself
```
## Rule Authoring
Rules live in `src/rules/<category>/` and follow this structure:
```typescript
export const rule: Rule = {
meta: {
id: 'category-rule-name', // Must match filename
name: 'Human Readable Name',
description: 'One-line summary',
category: 'CategoryName',
severity: 'error' | 'warning',
fixable: boolean,
deprecated: boolean,
since: '0.2.0', // Required for built-in rules, optional for custom rules
docs: {
recommended: boolean,
summary: 'Short summary for docs',
details: 'Full explanation for docs page',
examples: { incorrect: [...], correct: [...] },
},
},
create(context: RuleContext) {
// Validation logic — call context.report() for violations
},
};
```
### Adding a New Rule
1. Create `src/rules/<category>/<rule-name>.ts` following the pattern above
2. Create `tests/rules/<rule-name>.test.ts` with passing/failing cases
3. Run `npm run generate:types` to register the rule in `src/rules/index.ts` and `src/rules/rule-ids.ts`
4. Run `npm run docs:generate` to create the website documentation page
5. Run `npm run check:rule-coverage` to verify docs/tests exist
**Do NOT hand-edit `src/rules/index.ts` or `src/rules/rule-ids.ts`** — these are auto-generated.
### Shared Utilities for Rule Authors
Rules that parse Markdown/YAML content **must** use shared utilities. Do not hand-roll parsing logic.
| Utility | Purpose | Import from |
|---|---|---|
| `extractFrontmatter<T>()` | Parse YAML frontmatter into typed object | `utils/formats/markdown` |
| `extractBodyContent()` | Get content after frontmatter closing `---` | `utils/formats/markdown` |
| `stripCodeBlocks()` | Remove fenced + inline code blocks (preserves line count) | `utils/formats/markdown` |
| `getFrontmatterFieldLine()` | Find line number for a specific frontmatter field | `utils/formats/markdown` |
| `containsEnvVar()` | Check if string contains `${VAR}` or `$VAR` placeholder | `utils/patterns` |
| `isValidSemver()` | Validate semver format | `utils/patterns` |
| `escapeRegExp()` | Escape string for safe use in `new RegExp()` | `utils/patterns` |
| `isImportPath()` | Distinguish `@import` paths from decorators/emails | `utils/patterns` |
| `ENV_VAR_PLACEHOLDER_RE` | Regex constant for env var placeholders | `utils/patterns` |
| `SEMVER_RE` | Regex constant for full semver validation | `utils/patterns` |
| `HEADING_RE` | Regex constant for markdown headings | `utils/patterns` |
**Anti-patterns (enforced by `npm run check:rule-patterns`):**
- Do not use `lastIndex` with global regex -- use `matchAll()` instead
- Do not use `exec()` in while loops -- use `matchAll()` instead
- Do not strip code blocks with `/```[\s\S]*?```/g` -- use `stripCodeBlocks()`
- Do not split frontmatter with `.split('---')` -- use `extractBodyContent()`
- Do not import `js-yaml` directly -- use `extractFrontmatter()`
- Do not use `url.includes('$')` for env var detection -- use `containsEnvVar()` from `utils/patterns`
- Do not duplicate env var placeholder regex -- use `containsEnvVar()` or `ENV_VAR_PLACEHOLDER_RE`
- Do not inline `escapeRegExp` -- use `escapeRegExp()` from `utils/patterns`
- Do not duplicate semver regex -- use `isValidSemver()` or `SEMVER_RE` from `utils/patterns`
Pre-parsed data is also available on `context.frontmatter`, `context.bodyContent`, and `context.contentWithoutCode` (lazy-computed).
## Schemas
- **Zod schemas**: `src/schemas/` — used at runtime for validation
- **JSON schemas**: `schemas/` — generated from Zod via `npm run generate:json-schemas`
- Keep in sync: `npm run check:schema-sync` verifies Zod and JSON schemas match
## Test Structure
```text
tests/
rules/ # One test file per rule
validators/ # Validator integration tests
api/ # Public API tests
schemas/ # Schema validation tests
fixtures/ # Test project fixtures
helpers/ # Shared test utilities
integration/ # End-to-end CLI tests
```
Coverage thresholds: 70% branches, 80% functions/lines/statements.
## Key Directories
- `src/api/` — public programmatic API (`ClaudeLint` class, formatters). Keep stable.
- `src/cli/` — CLI commands (commander.js)
- `src/validators/` — orchestrate rules by component type (skills, hooks, settings, etc.)
- `src/utils/` — shared utilities (markdown parsing, frontmatter extraction, etc.)
- `src/types/` — TypeScript type definitions
## Rule Message Guidelines
Messages in `context.report({ message })` are shown in the default CLI table. They must be concise problem statements.
**Max length**: 100 characters (enforced by `npm run check:message-length`).
**Message content rules** (enforced by `npm run check:message-content`):
- State the problem only. No fix instructions, rationale, or examples.
- Do not start with imperative verbs (Add, Use, Create, Remove, Consider).
- Do not include "so that", "to ensure", "which means", "e.g.,", "for example".
- Do not dump lists of valid values. Put those in the `fix` field or `docs.howToFix`.
**Where content belongs**:
| Content type | Field | Shown in |
|---|---|---|
| Problem statement | `message` | Default table output |
| How to fix | `fix` or `docs.howToFix` | `--explain` mode (Fix:) |
| Why it matters | `docs.rationale` | `--explain` mode (Why:) |
| Full explanation | `docs.details` | `claudelint explain <rule>` |
| Code examples | `docs.examples` | `claudelint explain <rule>` |
**Good**: `File exceeds 40KB limit (50001 bytes)`
**Bad**: `File exceeds 40KB limit (50001 bytes). Split content into smaller files using @imports to keep the file manageable.`
## Conventions
- All rule IDs are kebab-case and prefixed by category: `skill-name`, `hook-event-type`
- - Rules delegate to Zod schemas where possible (thin wrapper pattern)
+ - Rules delegate to Zod schemas where possible (thin wrapper pattern).
+ Thin-wrapper rules (e.g., `output-style-name`, `agent-events`) have empty `validate` functions
+ because the schema handles validation. They ARE disableable via config: schema errors with
+ matching rule IDs are deduplicated in `mergeSchemaValidationResult()`, the thin wrapper reports
+ with the ruleId, and `getResult()` respects `severity="off"`.
- `context.report()` takes `{ message, line?, column?, fix? }`
- Fixable rules must provide a `fix` function in the report