@clipboard-health/oxlint-config
Shared Oxlint configuration for Clipboard Health repositories.
Table of contents
Install
npm install @clipboard-health/oxlint-config
The config package includes @clipboard-health/oxlint-plugin for presets that use Clipboard Health
custom rules.
Repositories using the frontend preset must also install oxlint-plugin-react-doctor >= 0.9.12.
Requires oxlint >= 1.68.0: the config references rules that older oxlint versions do not register, which fails config parsing outright.
Usage
Use the package's TypeScript helper when a repo needs additive composition. Oxlint's built-in extends is useful for simple inheritance, but it does not let a repo safely append shared plugins, overrides, and similar array fields without re-specifying the shared values.
TypeScript config
Create an oxlint.config.ts in your repo root:
import {
base,
createOxlintConfig,
customRules,
typeAware,
vitest,
} from "@clipboard-health/oxlint-config";
import { defineConfig } from "oxlint";
export default defineConfig(
createOxlintConfig({
localConfig: {
categories: {
correctness: "error",
nursery: "off",
pedantic: "error",
perf: "error",
restriction: "error",
style: "off",
suspicious: "error",
},
ignorePatterns: [".agents", "coverage/", "node_modules/"],
options: {
denyWarnings: true,
typeAware: true,
typeCheck: true,
},
rules: {
"vitest/require-test-timeout": "off",
},
settings: {
node: {
version: ">=24.14.1",
},
},
},
presets: [base, customRules, typeAware, vitest],
}),
);
Available presets:
basecontractFixturescustomRulesfast(see Fast runs)frontendreactjeststrictandstrictTypeAware(opt in; see Strict presets)typeAwarevitest
Merge behavior:
plugins,jsPlugins,overrides, andignorePatternsappend in orderrules,settings,options,categories,env, andglobalsmerge left-to-rightlocalConfigalways wins over preset values when keys conflict
For React applications, compose the standalone frontend policy with the repository's test runner:
import { createOxlintConfig, frontend, typeAware, vitest } from "@clipboard-health/oxlint-config";
import { defineConfig } from "oxlint";
export default defineConfig(
createOxlintConfig({
localConfig: {
ignorePatterns: ["coverage/"],
},
presets: [frontend, typeAware, vitest],
}),
);
Keep repository architecture rules, import restrictions, and additional JavaScript plugins in
localConfig.
base is the organization-wide general JavaScript and TypeScript policy. frontend is the
standalone frontend application policy, including React, accessibility, and browser-facing
JavaScript rules. typeAware contains rules that require Oxlint's --type-aware mode, and vitest
adds the complete Vitest policy. Keep repository-specific exceptions in localConfig.
frontend registers the built-in react, jsx-a11y, import, oxc, typescript, and
unicorn plugins. typeAware registers typescript, including when composed with another
plugin preset without base.
[!IMPORTANT] Upgrading from versions that omitted these plugin registrations can expose existing lint violations. Run normal and type-aware lint when upgrading, and allow enough CI runner memory for the newly active type-aware analysis. Existing repo-local registrations can remain.
JSON config
Create an .oxlintrc.json in your repo root:
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"extends": ["./node_modules/@clipboard-health/oxlint-config/src/base.json"],
"categories": {
"correctness": "error",
"nursery": "off",
"pedantic": "error",
"perf": "error",
"restriction": "error",
"style": "off",
"suspicious": "error"
},
"ignorePatterns": [".agents", "coverage/", "node_modules/"],
"options": {
"denyWarnings": true,
"reportUnusedDisableDirectives": "error",
"typeAware": true,
"typeCheck": true
},
"settings": {
"node": {
"version": ">=24.14.1"
}
}
}
Use JSON only when simple inheritance is enough. JSON extends still works for shared presets, but repo-local array fields like plugins and overrides will not get the additive merge behavior provided by createOxlintConfig.
For repos using Vitest, extend from vitest.json instead of base.json to include the vitest plugin and rules:
{
"extends": ["./node_modules/@clipboard-health/oxlint-config/src/vitest.json"]
}
Override shared rules as needed:
{
"extends": ["./node_modules/@clipboard-health/oxlint-config/src/base.json"],
"rules": {
"no-console": "off"
}
}
Fast runs
base and frontend enable reportUnusedDisableDirectives: "error", which flags oxlint-disable
and eslint-disable comments that no longer suppress anything. The default config is therefore the
slow one. The option is only accurate on the full, type-aware run: without type information,
typescript/* rules cannot fire, so their directives look unused. The fast preset extends the
default by setting the option to "off" for quick non-type-aware local and editor runs.
| Config | options.reportUnusedDisableDirectives | Use for |
|---|---|---|
default (base or frontend) | "error" | The full type-aware lint run and CI |
default + fast | "off" | Quick non-type-aware local and editor runs |
fast sets nothing else, so the two configs are otherwise identical. Compose it last in presets;
localConfig still wins if a repository sets the option itself.
A repository keeps its rules in oxlint.config.ts and derives the fast config from it, so there is
one place to edit rules:
// oxlint.config.ts
import { base, createOxlintConfig, typeAware } from "@clipboard-health/oxlint-config";
import { defineConfig } from "oxlint";
export default defineConfig(
createOxlintConfig({
presets: [base, typeAware],
}),
);
// oxlint.config.fast.ts
import { fast } from "@clipboard-health/oxlint-config";
import { defineConfig } from "oxlint";
import defaultConfig from "./oxlint.config.ts";
export default defineConfig({ extends: [defaultConfig], ...fast });
{
"scripts": {
"lint": "oxlint --type-aware --type-check",
"lint:fast": "oxlint --config oxlint.config.fast.ts"
}
}
lint loads oxlint.config.ts and runs the type-aware rules with unused-directive reporting.
lint:fast selects the fast config and omits --type-aware, so the typeAware preset's rules do
not run. Keep typeAware and typeCheck out of the config options and on the lint command line,
as above, so the fast config does not need to override them.
The JSON extends entry point base.json includes the option; set
options.reportUnusedDisableDirectives to "off" after extending it for a fast config.
Strict presets
These rules catch real bugs but fire often in legacy code, so they are opt-in. Adopt them per repository: add the presets, then suppress existing violations inline in the same change so new code meets the bar.
| Preset | Rule | Catches |
|---|---|---|
strict | @clipboard-health/no-ambiguous-positional-parameters | Two or more positional parameters where two share a type (callers can swap them) or one is a boolean (unreadable at call sites). A function with a single parameter is exempt, and so are test files. |
strictTypeAware | typescript/strict-boolean-expressions | Truthiness checks on numbers and enums, where 0 reads as missing. Strings stay allowed: treating "" as missing is usually intended. |
presets: [base, customRules, strict, typeAware, strictTypeAware, vitest],
Give each suppression a reason: // oxlint-disable-next-line <rule> -- <reason>.
Categories
This config is tuned for agent-written code: enforce rules that prevent bugs or help agents work faster, ignore rules that only encode human style preferences. Categories are applied as a denylist (enable the category, then disable individual rules that are noise) rather than an allowlist, so new bug-catching rules in a category are adopted automatically as oxlint ships them.
| Category | Setting | Why |
|---|---|---|
correctness, suspicious, perf | error | Likely bugs and performance problems. |
restriction | error | Carries high-value guardrails: typescript/no-explicit-any, typescript/no-non-null-assertion, no-var, unicorn/no-abusive-eslint-disable. |
pedantic | error | Carries eqeqeq, the typescript/no-unsafe-* family, typescript/no-misused-promises, typescript/switch-exhaustiveness-check, array-callback-return. |
style | off | Idiomatic/consistency rules with no bug-prevention value. Rules kept are opted back in explicitly: deliberate consistency picks (curly, no-else-return) plus bug/security rules oxlint files under style (no-new-func, guard-for-in, import/no-mutable-exports). |
nursery | off | Rules still under development; too unstable for agent-written code. |
style and nursery are off, not denylisted, because their future rules are noise rather than signal — there is nothing worth adopting automatically. Every style rule kept is therefore listed explicitly in base.json.
What is shared
The package includes:
base.json: the backwards-compatible JSON preset for simpleextendsusagevitest.json: extendsbase.jsonwith the vitest plugin and rules for JSONextendsusagebasepreset: shared plugins, rules, and overrides exported for TypeScript compositionfrontendpreset: shared frontend JavaScript, React Doctor, React, and JSX accessibility policytypeAwarepreset: rules that require Oxlint's type-aware execution modereact,jest,vitestpresets: additive plugin presets for common repo typescontractFixturespreset: warning-level enforcement that MSW, Playwright, and exported mock fixtures are parsed by producer-owned contract response schemascustomRulespreset: registers the ESLint-independent Clipboard plugin and reproduces the shared controller, module, contract, cross-contract-import, and contract-package-test enforcement scopes. Set rule options, such as the controller rule'sforbidTsRestHandler, with a local overridecreateOxlintConfig: helper for composing presets with repo-local config
Switch fallback policy
typescript/switch-exhaustiveness-check remains an error with
considerDefaultExhaustiveForUnions: true. An explicit default defines the policy for unmatched
values: rejecting them, returning an optional result, or applying a fallback can all be intentional.
This also permits defaults for legacy values that runtime data contains but TypeScript does not
represent. The rule still reports omitted union members when a switch has no default.
This is a deliberate tradeoff: adding a union or enum member does not trigger a diagnostic at a
switch that already has a default. Repositories or closed dispatchers that need every new member
reviewed can opt into considerDefaultExhaustiveForUnions: false with a local rule override. Keep
runtime fallbacks where their contracts need them; do not introduce throws merely to satisfy a lint
check. An explicit default does not prove that its business policy is correct.
Requiring repeated empty labels before a fallback conflicts with unicorn/no-useless-switch-case
and makes complexity count those labels as extra paths. Both guards remain enabled. See the
upstream option documentation
for the stricter future-member behavior. Upgrade to a published package release before removing a
consumer's local rule disable.
Intentionally disabled rules
base.json cannot hold comments, so the reasoning for non-obvious "off" entries lives here (audited against clipboard-health on oxlint 1.60, 2026-06-11):
no-throw-literal(TypeScript files only):typescript/only-throw-errorreplaces this syntax-only rule and uses types to recognize Error values. The base rule can reject deferred Error values held in typed variables. The upstream replacement guidance requires disabling the base rule to avoid incorrect diagnostics. The override covers.ts,.tsx,.mts, and.cts; JavaScript retainsno-throw-literal, and TypeScript retainsonly-throw-errorfor real non-Error throws. Run type-aware lint in CI. After upgrading to a published release with this override, consumers can remove a repository-wideno-throw-literal: off; an explicit matching local file override can opt back into the syntax-only rule.typescript/strict-void-return: TypeScript deliberately permits a value-returning function where avoidcallback is expected, because the caller discards the result. This supports ordinaryforEachcallbacks that callSet.add,Map.set, orArray.push, and callback APIs that return a synchronous handle. The rule rejects those valid uses; itsallowReturnAnyoption only relaxesanyreturns. Keepno-misused-promisesandno-floating-promisesenabled for async misuse. TypeScript still rejects a value returned from a function explicitly declared to returnvoid.typescript/no-unnecessary-template-expression: simplifying string expressions inside template literals is a style preference. Explicit string normalization and string-enum formatting are allowed. Keeprestrict-template-expressionsandno-base-to-stringenabled to detect unsafe interpolation instead.typescript/no-unnecessary-type-conversion: removingNumber,String,Boolean, or.toString()conversions based on a declared type is a style preference. At boundaries where runtime data does not match its declared type, removal can change the value or its type. Explicit coercion is allowed; it does not replace runtime schema validation or the unsafe-operation guards.typescript/no-unnecessary-boolean-literal-compare: comparisons such asvalue === true,value === false, andvalue !== falsecan encode an explicit boundary policy even when a static type saysboolean. At runtime, feature-flag providers and persisted records can still supply non-boolean, missing, or nullable values; simplifying these comparisons changes how those values are handled. Keep the comparisons intact and turn off this redundancy diagnostic in both shared presets. The tradeoff is losing the warning for comparisons over validated booleans; other type-aware correctness and unsafe-operation rules remain enabled.typescript/no-useless-default-assignment: a required static type does not prove that runtime data contains the field. Removing an initializer can change the value when database records, API responses, feature flags, or session payloads are incomplete. Keep these runtime defaults; this rule only signals a default that appears unreachable by its type. Disabling it accepts losing that redundant-code signal in validated code so defensive defaults remain available at unvalidated boundaries. The compiler and other type-aware correctness and unsafe-operation rules remain enabled.no-extra-boolean-cast:Boolean(value)in a condition makes numeric and enum truthiness explicit. Removing it is runtime-equivalent for an ordinary condition, but reintroduces thetypescript/strict-boolean-expressionsdiagnostic because that rule rejects implicit numeric or enum truthiness. Treating the explicit conversion as redundant style conflicts with that type-aware policy; keep the strict boolean guard and leave this stylistic rule off in both the base and standalone frontend presets.typescript/no-invalid-void-type: valid completion APIs use generic calls such asPromise.withResolvers<void>()andhttp.get<void>(). Even withallowInGenericTypeArguments: true, the rule rejects call-expression type arguments; this restriction is inherited from typescript-eslint, rather than an Oxlint port difference. Changing these APIs toundefinedcan change their public signatures: avoidresolver permitsresolve()without an argument. Keep the other TypeScript correctness and unsafe-operation guards enabled.typescript/consistent-return: optional lookups and filtered callbacks legitimately mix bare returns with value returns; the rule also reports catch paths that call a declaredneverhelper. ItstreatUndefinedAsUnspecifiedoption changes how explicitundefinedis classified, rather than recognizing these contracts. Upstream recommends TypeScript'snoImplicitReturnscompiler option instead. Enable that option and run compiler checks in CI;strictalone does not enable it. Repositories without that compiler guard must keep an explicit local"typescript/consistent-return": "error"until they adopt a replacement.array-callback-returnremains enabled for missing callback results. Existing suppressed return-type errors and ambiguous fallthrough contracts remain code debt.typescript/no-unsafe-enum-comparison: named constants such as HTTP statuses are compared with wire numbers and serialized strings. The shared TypeScript guidance also prefers const objects over enums. This rule instead requires matching nominal enum types, rejecting those boundary comparisons and input validation. Disabling it also permits matching raw enum literals: it does not enforce using a named member in that case. The compiler still rejects incompatible distinct enums and out-of-range literals; it does not replace every check this rule can make. Repositories that require nominal enum identity can explicitly set this rule to"error"inlocalConfig.rules. The upstream guidance describes this tradeoff; the rule has no configuration option for scalar-boundary comparisons. Other enum guards, including duplicate-value detection, remain enabled.typescript/consistent-type-imports: oxlint's port is notemitDecoratorMetadata-aware (even type-aware with the flag in the tsconfig chain), so it flags NestJS constructor-injected classes as type-only. Applying its fix converts DI tokens toimport type, which SWC erases fromdesign:paramtypesmetadata, breaking injection at runtime. Do not enable in NestJS repos until oxlint matches typescript-eslint's decorator handling.jest/no-confusing-set-timeout(jest preset): false-positive explosion — when a jest setup file callsjest.setTimeout, the rule flags spec files that contain nosetTimeoutcall at all (265k diagnostics across 764 clean files). The vitest preset carries nojest/*keys at all: under oxlint ≥1.68 with only the vitest plugin enabled,jest/*config keys are silently ignored, and jest-plugin-only rules (such as this one andjest/prefer-ending-with-an-expect) cannot fire —vitest/no-confusing-set-timeoutis not a registered rule name, so an "off" entry for it fails config parsing.import/no-named-as-default/import/no-named-as-default-member: low signal; they flag legitimate default-export import patterns far more often than real mistakes (870 hits, ~0 bugs).vitest/no-conditional-in-test(vitest preset): the rule treats every conditional inside a test, including nested mock callbacks and table-driven input construction, as a violation. Stateful fakes, concurrency barriers, and conditional fixture properties need branches without weakening the assertions. The rule has no option to distinguish these cases; extracting the same branches into helpers only hides them from its test-ancestor heuristic. Keepvitest/expect-expect,vitest/valid-expect, andvitest/require-to-throw-messageenabled to check assertions.prefer-destructuring: style preference with no bug-prevention value; not worth the churn as an agent guardrail.
The following were turned off after an audit of 13 consumer repos (repo-local "off" entries, inline disable comments, and a run of the plain base + vitest presets on 7 repos, 2026-10-14):
no-await-in-loop: sequential awaits are usually intentional (rate limits, Mongo sessions, ordering). The rule pushes agents towardPromise.all, which can cause concurrency bugs. It had 1,007 inline disables across 9 repos.node/no-process-env: off locally in 8 of 13 repos. Repos that centralize configuration can turn it back on.oxc/no-map-spread: a micro-optimization. Its suggested fix mutates withObject.assign.unicorn/consistent-function-scoping: moving closures to module scope often makes code harder to read.unicorn/no-useless-undefined: conflicts with TypeScript signatures that need an explicitundefined.- Stylistic, in the restriction category:
no-plusplus,typescript/explicit-member-accessibility,typescript/no-import-type-side-effects,unicorn/import-style.
The same audit removed the explicit opt-ins for typescript/array-type and unicorn/filename-case. They are style rules, so the style category now keeps them off.
When adopting the callback and coercion defaults, first upgrade to a published release containing them, then remove redundant local off entries for typescript/strict-void-return, typescript/no-unnecessary-template-expression, and typescript/no-unnecessary-type-conversion. Repositories that require these stricter conventions can opt back in with local error overrides. Keep no-misused-promises and no-floating-promises enabled: allowing discarded synchronous results does not permit unhandled async failures. Keep restrict-template-expressions and no-base-to-string enabled for unsafe interpolation.
The test-file override in base.json disables the typescript/no-unsafe-* family, unbound-method, no-non-null-assertion, and restrict-template-expressions for test files: mocks, expect.any, and createMock make these rules ~90% noise in tests (mirrors the long-standing eslint-config override), while keeping them enforceable for production code. The JSON Vitest preset extends base.json, but the exported TypeScript vitest preset loader reads only its plugins and rules. The exported preset therefore carries the same typescript/unbound-method: off rule for the base test-file globs. A later localConfig can still turn the rule back on for selected tests.