# Add custom rules

Load your own oxlint-shaped plugins into a React Doctor scan. Use this for team conventions or project-specific detectors that do not belong in the core rule set. Custom rules share the same CLI output, JSON report, score, PR comments, and CI gate as built-in rules.

## When to use a custom plugin

Use a custom plugin when:

- You need a detector React Doctor does not ship
- The rule is team- or product-specific
- You want findings in the same report as built-in diagnostics

Use the [ESLint and oxlint plugins](/docs/configuration/eslint-and-oxlint-plugins) page when you want React Doctor’s built-in rules inside ESLint or oxlint instead.

## Write a plugin module

Export `meta.name` and a `rules` map. Each rule implements `create(context)` and returns oxlint visitors:

```js
// lint/team-conventions.cjs
module.exports = {
  meta: {
    name: "team-conventions",
  },
  rules: {
    "no-forbidden-word": {
      create(context) {
        return {
          JSXText(node) {
            if (typeof node.value !== "string") return;
            if (!node.value.includes("FORBIDDEN")) return;
            context.report({
              node,
              message:
                "team policy: FORBIDDEN is not allowed in JSX text",
            });
          },
        };
      },
    },
  },
};
```

`meta.name` becomes the plugin namespace in rule keys and diagnostics. Relative paths resolve from the directory that contains your `doctor.config.*` file, not from the scan root.

You can also point `plugins` at an npm package that default-exports the same `{ meta, rules }` shape.

## Enable the plugin in config

Add the plugin path or package name, then opt in each rule under `rules`:

```json
{
  "$schema": "https://react.doctor/schema/config.json",
  "plugins": ["./lint/team-conventions.cjs"],
  "rules": {
    "team-conventions/no-forbidden-word": "error"
  }
}
```

Rule keys use `<plugin-name>/<rule-name>`. Plugin rules stay off until you set them to `error` or `warn`.

Severity, `ignore`, and `surfaces` work the same way they do for built-in rules. See [Config files](/docs/configuration/config-files).

## Run a scan

```bash
npx react-doctor@latest --verbose
```

Findings from your plugin appear with `plugin` set to `meta.name` and `rule` set to the rule id. JSON mode (`--json`) uses the same fields, so graders and scripts can key on `team-conventions/no-forbidden-word`.

## Load plugins from the Node API

Pass `plugins` and `rules` through batch `diagnose` config, or put them in on-disk `doctor.config.*` for single-directory `diagnose`:

```ts
import { diagnose } from "react-doctor/api";

const result = await diagnose({
  projects: [{ directory: "./apps/web" }],
  config: {
    plugins: ["./lint/team-conventions.cjs"],
    rules: {
      "team-conventions/no-forbidden-word": "error",
    },
  },
});
```

Relative plugin paths resolve from the config source directory for that project. See the [Node.js API](/docs/reference/node-api).

## Limits of custom plugins

Custom plugins are oxlint visitor rules with these limits:

- **Opt-in only**: a plugin rule never runs until `rules` enables it
- **AST visitors only**: project-level `scan` rules and React Doctor capability gates (`requires`, `disabledWhen`) stay built-in
- **Less diagnostic metadata**: user-plugin findings often fall back to the plugin and rule ids for title and category; built-in registry rules carry richer titles, categories, and tags
- **Unresolved plugins are skipped**: a missing path or package warns and the scan continues without that plugin

If a rule should ship for every React Doctor user, contribute it upstream instead of keeping it as a private plugin.
