# Lint Configuration

> **Scope of this page**
>
> This page documents the Zuplo **gateway** runtime (API, AI, and MCP gateways), not the Developer Portal.
>
> It applies to every Zuplo hosting option: Managed Edge, Managed Dedicated, and Self-Hosted. See [Hosting options](https://zuplo.com/docs/articles/hosting-options).

`zuplo lint` reads its settings from a `zuplo.lint.jsonc` file in the root of
your project. The file is optional: without it, the linter runs the
`zuplo:recommended` preset and fails only on errors.

Start the file with `$schema` and `version`:

```jsonc title="zuplo.lint.jsonc"
{
  "$schema": "https://cdn.zuplo.com/schemas/zuplo-lint.json",
  "version": 2,
  "extends": ["zuplo:recommended"],
}
```

`$schema` gives your editor completion and inline errors for every setting on
this page. `"version"` is the version of the configuration format; set it to
`2`.

The linter looks for `zuplo.lint.jsonc` first, then `zuplo.lint.json`. To use a
file somewhere else, pass its path with `--config`. The path is relative to the
directory you run the command from, not to `--dir`:

```bash
npx zuplo lint --config ./lint/strict.jsonc
```

The file is JSON with comments (JSONC), so you can annotate why a rule is off or
a path is skipped. Trailing commas are allowed.

## Full example

```jsonc title="zuplo.lint.jsonc"
{
  "$schema": "https://cdn.zuplo.com/schemas/zuplo-lint.json",
  "version": 2,

  // Rulesets to start from, applied in order
  "extends": [
    "zuplo:strict",
    ["spectral:owasp", "recommended"],
    "./lint/team-ruleset.json",
  ],

  "rules": {
    // A severity: "error", "warn", "info", "hint", or "off"
    "info-contact": "off",

    // A severity scoped to some routes
    "operation-description": {
      "severity": "error",
      "paths": ["/v1/**"],
      "excludePaths": ["/v1/internal/**"],
    },

    // A custom rule: require an API key policy on /v1 routes
    "require-api-key-v1": {
      "message": "v1 routes need an API key",
      "severity": "error",
      "paths": ["/v1/**"],
      "given": "$",
      "then": {
        "function": "zuplo:requirePolicy",
        "functionOptions": { "policyTypes": ["api-key"] },
      },
    },
  },

  // Change rules for some routes or files; later entries win
  "overrides": [
    {
      "paths": ["/health"],
      "rules": { "zuplo/require-policy": "off", "operation-tags": "off" },
    },
    {
      "files": ["config/legacy.oas.json"],
      "rules": { "operation-description": "warn" },
    },
  ],

  // The lowest severity that fails the run
  "failOn": "error",

  // Compare the OpenAPI files with a Git branch
  "breakingChanges": { "enabled": true, "baseline": "origin/main" },
}
```

## How settings combine

```txt
extends (left to right) → rules → overrides (in order) → --fail-on flag
```

1. `extends` builds the starting set of rules. When two rulesets define the same
   rule, the later one wins.
2. `rules` changes the severity of those rules, limits them to some routes, or
   adds new ones.
3. `overrides` changes severities for the routes or files they match. When
   several overrides match a result, the last one wins.
4. `--fail-on` on the command line replaces [`failOn`](#failon) for one run.

## Severities

Every rule reports at one severity:

| Severity  | Meaning                                                                    |
| --------- | -------------------------------------------------------------------------- |
| `"error"` | A defect. Fails the run under the default `failOn`.                        |
| `"warn"`  | A likely problem. Fails the run only when `failOn` is `"warn"`.            |
| `"info"`  | Worth knowing. Reported, counted under `info` in the summary, never fails. |
| `"hint"`  | A suggestion. Reported like `info`, never fails.                           |
| `"off"`   | The rule doesn't run, or doesn't report on the routes it's off for.        |

## Options

### `extends`

The rulesets to start from. The default is `["zuplo:recommended"]`. An entry is
one of these:

**A Zuplo preset.** Each preset combines Zuplo's own checks with a reviewed set
of OpenAPI rules that catch real defects without flooding a typical project.

| Preset              | Adds                                                                                                                                                                    |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `zuplo:recommended` | The core checks: `operationId`, descriptions, tags, path parameters, undefined policies, route labels, policy schemas, duplicate operations, unsafe Markdown, and more. |
| `zuplo:strict`      | Everything in recommended, plus required authentication and rate limiting, CORS on every route, API info, error responses, and documentation completeness.              |
| `zuplo:security`    | Everything in recommended, plus required authentication and rate limiting, both as errors.                                                                              |

**A vacuum or Spectral built-in ruleset.** These bring in the full catalog of
general OpenAPI rules:

| Ruleset                            | Contains                  |
| ---------------------------------- | ------------------------- |
| `vacuum:oas` or `spectral:oas`     | General OpenAPI rules.    |
| `vacuum:owasp` or `spectral:owasp` | OWASP API security rules. |

On their own, these sets turn on that set's recommended rules. Write the entry
as a pair to pick a mode instead, as in Spectral:

```jsonc
{
  "version": 2,
  "extends": [
    "zuplo:recommended",
    ["spectral:owasp", "all"], // every OWASP rule
  ],
}
```

| Mode            | Effect                                               |
| --------------- | ---------------------------------------------------- |
| `"recommended"` | The set's recommended rules. The default.            |
| `"all"`         | Every rule in the set.                               |
| `"off"`         | Removes the set's rules that an earlier entry added. |

Some general OpenAPI rules don't fit Zuplo projects. For example,
`oas3-api-servers` reports every spec because Zuplo supplies the servers, and
`paths-kebab-case` reports URLPattern paths such as `/users/:id`. Turn those off
in `rules` when you extend `vacuum:oas`, or start from a Zuplo preset, which
leaves them out.

**A local ruleset file.** A `.json` or `.jsonc` file in the Spectral ruleset
format, with a path relative to `zuplo.lint.jsonc`. Use it to share rules
between projects. See
[Custom lint rules](./api-lint-custom-rules.mdx#share-rules-with-a-ruleset-file).

Run `npx zuplo lint --list-rules` to see every rule and which rulesets include
it. The [rules reference](./api-lint-rules.mdx) describes the rules in the Zuplo
presets.

### `rules`

Rule settings, keyed by rule ID. A value is either a severity or an object.

A severity changes the rule everywhere:

```jsonc
{
  "version": 2,
  "rules": {
    "operation-tags": "off",
    "operation-description": "error",
  },
}
```

An object changes the severity and limits which routes the rule reports on:

```jsonc
{
  "version": 2,
  "rules": {
    "operation-description": {
      "severity": "error",
      "paths": ["/v1/**"],
      "excludePaths": ["/v1/internal/**"],
    },
  },
}
```

| Field          | Description                                                                  |
| -------------- | ---------------------------------------------------------------------------- |
| `severity`     | The rule's severity. Leave it out to keep the severity the rule already has. |
| `paths`        | Report the rule only on these API paths.                                     |
| `excludePaths` | Don't report the rule on these API paths.                                    |

Path patterns work the same everywhere in this file:

- A plain path matches that route exactly, as written in the OpenAPI file:
  `"/users/{userId}"`.
- A path ending in `/**` matches that path and every path beneath it.
  `"/internal/**"` matches `/internal` and `/internal/jobs`, but not
  `/internal2`.

A rule with `paths` doesn't report results that aren't tied to a route, such as
a missing `info.description` or a `policies.json` schema error.

An object with `given` and `then` defines a new rule. See
[Custom lint rules](./api-lint-custom-rules.mdx).

A rule ID that no ruleset in `extends` defines stops the run with exit code `2`,
and the error suggests the closest ID. To configure a rule, extend a ruleset
that includes it: `"zuplo/require-cors-policy"` is only available when you
extend `zuplo:strict`.

`zuplo/require-policy` is the rule that enforces
[`requiredPolicies`](#requiredpolicies). Setting it applies the severity to
every required policy, and `"off"` turns all of them off. Use the base ID here;
`zuplo/require-policy-warn`, which appears in results, isn't a rule you
configure.

### `overrides`

Change rule severities for some routes or OpenAPI files. Use an override when a
group of routes is deliberately different from the rest of the API: public
endpoints, a health check, a legacy spec you can't change yet.

```jsonc
{
  "version": 2,
  "overrides": [
    {
      "paths": ["/health", "/status"],
      "rules": { "zuplo/require-policy": "off" },
    },
    {
      "files": ["config/legacy.oas.json"],
      "rules": {
        "operation-description": "off",
        "operation-tags": "off",
      },
    },
  ],
}
```

| Field   | Description                                                                                                                   |
| ------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `paths` | API paths the override applies to.                                                                                            |
| `files` | OpenAPI files the override applies to, relative to the project directory. `*` matches within one folder: `config/*.oas.json`. |
| `rules` | Severities to apply, keyed by rule ID.                                                                                        |

An override needs `paths`, `files`, or both. With both, a result must match
both.

An override can also turn a rule on. With `"operation-tags": "off"` in `rules`
and `{ "paths": ["/v1/**"], "rules": { "operation-tags": "warn" } }` in
`overrides`, the rule reports only on `/v1` routes. An override works within a
rule's own `paths` and `excludePaths`, and can't turn a rule on for a path in
`ignorePaths`.

### `failOn`

The lowest severity that fails the run with exit code `1`.

| Value     | The run fails when                                                      |
| --------- | ----------------------------------------------------------------------- |
| `"error"` | There is at least one error, or a breaking change reported as an error. |
| `"warn"`  | There is at least one error, warning, or breaking change.               |
| `"none"`  | Never. The linter reports results and exits with `0`.                   |

The default is `"error"`. `info` and `hint` results never fail a run. Any other
value stops the run with exit code `2`. The `--fail-on` flag overrides this
value for a single run.

### `ignorePaths`

API paths whose route results the linter drops for every rule. An override can't
turn a rule back on for an ignored path.

```jsonc
{
  "version": 2,
  "ignorePaths": ["/health", "/internal/**"],
}
```

`ignorePaths` also applies to
[automatic fixes](./api-lint.mdx#fix-problems-automatically). Results that
aren't tied to a route still appear: `policies.json` schema results, `info`
rules, and [breaking changes](./api-lint-breaking-changes.mdx). To turn off only
some rules for a route, use [`overrides`](#overrides).

### `include`

The OpenAPI files to lint, relative to the project directory. By default, the
linter lints every file directly in `config/` that ends in `.oas.json`; it
doesn't look in subdirectories.

```jsonc
{
  "version": 2,
  "include": ["config/public-api.oas.json"],
}
```

Every entry must end in `.oas.json`, must be a relative path, and must stay
inside the project directory. Glob patterns aren't supported, so list each file.
A file that doesn't exist fails the run with exit code `2`.

The CLI still requires at least one `.oas.json` file directly in `config/`, so
use `include` to narrow the files that are linted, not to point at files
elsewhere.

`include` controls which OpenAPI files are linted. The linter always validates
`config/policies.json` when it exists.

### `requiredPolicies`

Policies that every route must include. The `zuplo:strict` and `zuplo:security`
presets each define a default set; `requiredPolicies` replaces those defaults
with your own list.

```jsonc
{
  "version": 2,
  "extends": ["zuplo:recommended"],
  "requiredPolicies": [
    {
      "id": "require-auth",
      "policyTypes": ["api-key", "open-id-jwt-auth"],
      "direction": "inbound",
      "severity": "error",
      "message": "Routes must have an authentication policy",
    },
    {
      "id": "require-rate-limit",
      "policyTypes": ["rate-limit"],
      "severity": "warn",
      "message": "Routes should have a rate limiting policy",
    },
  ],
}
```

Unlike other rules, `requiredPolicies` works with any preset, including
`zuplo:recommended`. Each requirement applies to every route; to require a
policy on only some routes, use [`overrides`](#overrides) or a
[custom rule with `paths`](./api-lint-custom-rules.mdx#require-a-policy-on-some-routes).

Each entry has these fields:

| Field         | Description                                                                                               |
| ------------- | --------------------------------------------------------------------------------------------------------- |
| `id`          | A name for the requirement. Use it to keep track of entries; results report under `zuplo/require-policy`. |
| `policyTypes` | Policy types that satisfy the requirement. A route needs at least one policy whose type matches.          |
| `direction`   | `"inbound"` or `"outbound"`. Defaults to `"inbound"`.                                                     |
| `severity`    | `"error"` or `"warn"`. Defaults to `"error"`.                                                             |
| `message`     | The message to report. The linter appends the method and path of the route, for example `: get /todos`.   |

The linter matches each policy name in the route's `x-zuplo-route.policies` list
to its `policyType` in `config/policies.json`. An entry in `policyTypes` matches
that exact type and any type that starts with it followed by a dash, so
`"api-key"` matches `api-key-inbound`, and `"rate-limit"` matches
`rate-limit-inbound`. Matching starts at the beginning of the type, so
`"rate-limit"` doesn't match `complex-rate-limit-inbound`; list
`"complex-rate-limit"` separately if you use it.

Requirements with `"severity": "error"` report under the rule ID
`zuplo/require-policy`. Requirements with `"severity": "warn"` report under
`zuplo/require-policy-warn`.

To turn off the preset defaults without adding your own requirements, set an
empty list:

```jsonc
{
  "version": 2,
  "extends": ["zuplo:strict"],
  "requiredPolicies": [],
}
```

The defaults in `zuplo:strict` and `zuplo:security` accept these policy types:

- **Authentication:** `api-key`, `auth0-jwt-auth`, `clerk-jwt-auth`,
  `cognito-jwt-auth`, `curity-phantom-token`, `firebase-jwt-auth`, `jwt-auth`,
  `oauth2-pkce-auth`, `okta-jwt-auth`, `open-id-jwt-auth`, `propel-auth`,
  `stytch-jwt-auth`, `supabase-jwt-auth`
- **Rate limiting:** `rate-limit`, `complex-rate-limit`

### `breakingChanges`

Compares each OpenAPI file with the same file on a Git branch, tag, or commit,
and reports changes that can break existing API consumers, such as a removed
endpoint or a changed type.

```jsonc
{
  "version": 2,
  "breakingChanges": {
    "enabled": true,
    "baseline": "origin/main",
    "severity": "error",
  },
}
```

| Field      | Description                                                              |
| ---------- | ------------------------------------------------------------------------ |
| `enabled`  | Turns detection on. Defaults to `false`.                                 |
| `baseline` | The branch, tag, or commit SHA to compare against. Defaults to `"main"`. |
| `severity` | `"error"` or `"warn"`. Defaults to `"error"`.                            |

See [Breaking change detection](./api-lint-breaking-changes.mdx) for how to
choose a baseline, what counts as a breaking change, and how to run it in CI.

## Command-line flags and the configuration file

Flags take precedence over the configuration file for a single run:

| Flag        | Overrides                         |
| ----------- | --------------------------------- |
| `--config`  | Which configuration file to read. |
| `--fail-on` | [`failOn`](#failon).              |

All other settings come only from the configuration file. See the
[`zuplo lint` CLI reference](../cli/lint.mdx) for every flag.

## Next steps

- [Custom lint rules](./api-lint-custom-rules.mdx) — write your own rules and
  share them with a ruleset file.
- [Lint rules reference](./api-lint-rules.mdx) — what each rule checks.
- [Lint in CI/CD](./api-lint-ci-cd.mdx) — run the linter on every pull request.
