# How to Implement Custom Linting Rules in the DESIGN.md CLI

> Learn to implement custom linting rules in the DESIGN.md CLI. Create RuleDescriptors with Run methods and pass them to the lint function. Enhance your design documentation workflow.

- Repository: [Google Labs Code/design.md](https://github.com/google-labs-code/design.md)
- Tags: how-to-guide
- Published: 2026-06-28

---

**You can implement custom linting rules in the DESIGN.md CLI by creating a `RuleDescriptor` with a `run` method that returns `Finding` objects, then passing it to the `lint()` function via the optional `rules` array in `LintOptions`.**

The DESIGN.md CLI from google-labs-code provides a pluggable linting architecture that allows you to enforce project-specific design system conventions without modifying the core source code. The public API exposes the `lint` function and `DEFAULT_RULES` from [`packages/cli/src/linter/index.js`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/linter/index.js), enabling you to compose custom rule sets alongside the built-in validators.

## Understanding the Linting Architecture

The DESIGN.md CLI implements a modular rule system centered on two core abstractions defined in [`packages/cli/src/linter/linter/rules/types.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/linter/linter/rules/types.ts).

**`RuleDescriptor`** defines the shape of a lint rule. It contains a `run` function that receives the fully-resolved `DesignSystemState` and returns an array of `Finding` objects, plus an optional default `severity` level.

**`LintRule`** represents the executable form of a descriptor. The CLI converts `RuleDescriptor` objects into `LintRule` instances using the **`toLintRule`** helper in [`packages/cli/src/linter/linter/rules/index.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/linter/linter/rules/index.ts).

The linting pipeline is orchestrated by the **`lint`** function in [`packages/cli/src/linter/lint.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/linter/lint.ts) (lines 25-28). This function accepts an `options` object containing an optional `rules` field—a `LintRule[]` array that overrides or extends the default rule set. The actual execution happens in **`runLinter()`** (located in [`packages/cli/src/linter/linter/runner.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/linter/linter/runner.ts)), which iterates over the provided rule list and aggregates findings into a comprehensive `LintReport`.

The built-in command at [`packages/cli/src/commands/lint.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/commands/lint.ts) does not expose a CLI flag for custom rules, requiring you to invoke the linter programmatically or build a custom command wrapper.

## Creating a Custom Rule Descriptor

To implement a custom linting rule, create a module that exports a `RuleDescriptor` implementing the `run(state)` method.

The following example defines a rule that prohibits tokens named "foo":

```ts
// File: packages/cli/src/custom-rules/no-foo-token.ts
import type { RuleDescriptor } from '../linter/linter/rules/types.js';
import type { DesignSystemState } from '../linter/model/spec.js';
import type { Finding } from '../linter/linter/spec.js';

/**
 * Disallow any token named "foo".
 */
export const noFooTokenRule: RuleDescriptor = {
  severity: 'error',
  run(state: DesignSystemState): Finding[] {
    const findings: Finding[] = [];
    
    for (const [name, token] of Object.entries(state.tokens ?? {})) {
      if (name === 'foo') {
        findings.push({
          message: 'Token "foo" is not allowed – use a more descriptive name.',
          path: ['tokens', name],
        });
      }
    }
    
    return findings;
  },
};

```

Key implementation details:

- The `run` method receives a `DesignSystemState` object containing the parsed design system model, including the `tokens` map.
- Findings must include a `message` string and may include a `path` array indicating the location of the violation.
- If you omit `severity` in individual findings, the descriptor's top-level `severity` value applies automatically when the CLI converts the descriptor to a `LintRule`.

## Running the Linter with Custom Rules

Since the default CLI command does not support custom rule paths, create a Node script that imports the `lint` API and supplies your rule descriptors.

```ts
// File: scripts/run-custom-lint.ts
import { readFile } from 'node:fs/promises';
import { lint, DEFAULT_RULES } from '../packages/cli/src/linter/index.js';
import { noFooTokenRule } from '../packages/cli/src/custom-rules/no-foo-token.js';

async function main() {
  const designMd = await readFile('examples/totality-festival/DESIGN.md', 'utf-8');
  
  const allRules = [...DEFAULT_RULES, noFooTokenRule];
  
  const report = lint(designMd, { rules: allRules });
  
  console.log('Lint summary:', report.summary);
  console.log('Findings:');
  for (const f of report.findings) {
    console.log(`- [${f.severity}] ${f.message} (path: ${f.path?.join(' → ')})`);
  }
}

main().catch(err => {
  console.error('Lint failed:', err);
  process.exit(1);
});

```

This script:

1. Loads the DESIGN.md content as a string.
2. Merges `DEFAULT_RULES` with your custom rule descriptor.
3. Passes the combined array to `lint()` via the `options.rules` parameter.
4. Processes the returned `LintReport`, which contains `findings`, a `summary` of severities, the resolved design-system model, and Tailwind output.

To replace the default rules entirely rather than extending them, pass only your custom descriptors: `lint(content, { rules: [myRule] })`.

## Exposing Custom Rules via CLI Flags (Optional)

If you prefer command-line invocation over a Node script, extend the existing CLI command by copying the structure from [`packages/cli/src/commands/lint.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/commands/lint.ts) and adding a `--custom-rules` flag:

```ts
// Example fragment added to command definition
customRules: {
  type: 'string',
  description: 'Path to a module exporting a RuleDescriptor or an array thereof',
},
// Inside the run handler:
const custom = await import(path.resolve(args.customRules));
const extra = Array.isArray(custom) ? custom : [custom];
const report = lint(content, { rules: [...DEFAULT_RULES, ...extra] });

```

This approach dynamically imports the user-provided module, extracts the exported descriptor(s), and merges them with `DEFAULT_RULES` before executing the lint pipeline.

## Summary

- **Custom rules** in the DESIGN.md CLI are implemented as `RuleDescriptor` objects with a `run(state)` method that returns `Finding` arrays.
- The **`lint()`** function in [`packages/cli/src/linter/lint.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/linter/lint.ts) accepts a `rules` option that overrides or extends the default rule set.
- **`DEFAULT_RULES`** is exported from [`packages/cli/src/linter/linter/rules/index.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/linter/linter/rules/index.ts) and can be spread into custom rule arrays.
- The default CLI command does not expose custom rules, so you must invoke the API programmatically or build a custom command wrapper.
- **`runLinter()`** in [`packages/cli/src/linter/linter/runner.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/linter/linter/runner.ts) executes the rules and aggregates results into a `LintReport`.

## Frequently Asked Questions

### How do I override the default severity for a custom rule?

Set the `severity` property at the top level of your `RuleDescriptor` object. When the CLI converts your descriptor to a `LintRule` using the `toLintRule` helper in [`packages/cli/src/linter/linter/rules/index.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/linter/linter/rules/index.ts), it applies this default severity to any findings that do not explicitly specify their own severity level.

### Can I disable specific default rules while keeping others?

Yes. Import `DEFAULT_RULES` from [`packages/cli/src/linter/linter/rules/index.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/linter/linter/rules/index.ts), filter the array to remove unwanted rules, and pass the filtered array to `lint()` via the `options.rules` parameter. The `lint` function uses your provided array exactly as given, without merging back to the default set.

### What data is available in the `DesignSystemState` parameter?

The `DesignSystemState` object (defined in [`packages/cli/src/linter/model/spec.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/linter/model/spec.ts)) contains the fully-resolved design system model, including the `tokens` map and other parsed DESIGN.md structures. You can inspect token names, values, types, and cross-references to enforce project-specific constraints.

### How do I format the lint output for CI pipelines?

The `lint()` function returns a `LintReport` object containing a `findings` array and a `summary` object. Iterate over `report.findings` to generate JSON, JUnit XML, or custom formatted output. Each finding includes `message`, `severity`, and optional `path` properties for precise error location reporting.