Linter

Bun's fast, built-in linter for JavaScript, TypeScript, and JSX

Bun ships with a fast, built-in linter. There's nothing to install and nothing to configure.

  • 1,000+ rules from ESLint, typescript-eslint, React, import, unicorn, jsx-a11y, Jest, Vitest, and more
  • Type-aware rules, without the typescript package
  • Auto-fix with --fix
  • Works with your existing ESLint or oxlint config

Run the linter#

terminal
bun lint
1 | export function total(prices: number[]) {
2 |   let sum = 0;
3 |   for (let i = 0; i < prices.length; i--) {
      ^
error: The update clause in this loop moves the variable in the wrong direction.  for-direction
    at src/cart.ts:3:3

5 |   }
6 |
7 |   let tax = 0.2;
          ^
warn: 'tax' is never reassigned. Use 'const' instead.  prefer-const
   at src/cart.ts:7:7

2 problems (1 error, 1 warning)
1 can be fixed with --fix
Linted 1 file [5.17ms]

bun lint checks every .js, .jsx, .ts, and .tsx file in the current directory (plus the .mjs, .cjs, .mts, .cts variants). It skips node_modules and anything in your .gitignore or .git/info/exclude.

To lint specific files, pass paths or globs.

terminal
bun lint src "test/**/*.test.ts"

An argument that starts with ! leaves files out of what the other arguments select, as it does for bun fmt. This holds with an ESLint or oxlint config too.

terminal
bun lint src "!src/generated" "!**/*.test.ts"

The exit code is 1 if there are errors and 0 if there are only warnings.

Fix problems#

Many rules can fix what they report. --fix applies those fixes and writes the files.

terminal
bun lint --fix

--fix is for changes that keep what your program does and keep your comments. Where a rule's fix is known to alter behavior or drop a comment, the problem is still reported, but only --fix-suggestions or --fix-dangerously applies the fix, even if ESLint or oxlint apply it with --fix. Each rule's page says which flag it needs.

Default rules#

With no config, bun lint turns on the rules that catch real bugs:

  • Every file: ESLint's recommended rules
  • TypeScript: typescript-eslint's recommended rules
  • React: rules-of-hooks, exhaustive-deps, and jsx-key, if react is in your package.json
  • Tests: leftover test.only, expect() without a matcher, un-awaited .resolves, and a few more, in files that import from bun:test

Likely bugs are errors. Unused code, style suggestions, and code that people often write on purpose, like while (match = re.exec(text)), are warnings. A warning doesn't fail the run.

A few defaults differ from ESLint's, so that ordinary code isn't reported:

  • require() is allowed in TypeScript files. Bun runs import and require() in the same file.
  • A name that starts with _ may be unused.
  • done && done(); and ok ? resolve() : reject(); are fine as statements.
  • A no-break space or a full-width space is only reported in code, not in text, comments or JSX.
  • process.exit() ends a case, and // fall-through or // no break marks one that goes on.
  • {false && <Debug />} is a fine way to switch something off for a while.
  • An array of JSX elements needs keys only where React renders it as a list.
  • import React counts as used in a file with JSX.

Each rule's page says what differs and how to change it.

  • In .d.ts files only the errors run.
  • An eslint-disable comment for a rule that is off is left alone.

Configure rules#

Rules live in bunfig.toml. Every built-in rule works the same way, with nothing to install. The exceptions are the rules for JSON and Markdown, which need an ESLint config.

bunfig.toml
[lint.rules]
"no-console" = "warn"
"prefer-const" = "off"
"eqeqeq" = ["error", "smart"] # with options
"unicorn/prefer-node-protocol" = "error"
"@typescript-eslint/no-explicit-any" = "off"

An option you don't set keeps its default.

To create the section, run bun lint --init.

A typo is an error, not a silent no-op:

2 | "no-consle" = "warn"
    ^
error: Unknown rule "no-consle"
  Did you mean "no-console"?
    at bunfig.toml:2:1

Find a rule#

Browse all rules, or look one up from the terminal. In most terminals you can also click the rule name in a report.

terminal
bun lint --rules                 # list every rule
bun lint --explain prefer-const  # what it does, examples, options

Add an ESLint plugin#

Need a rule that isn't built in? Load any ESLint plugin, from npm or your own. The default rules stay on.

bunfig.toml
[lint]
plugins = ["eslint-plugin-perfectionist", "./lint/plugin.js"]

[lint.rules]
"perfectionist/sort-imports" = "error"
"local/no-todo" = "warn"

Turn on a group of rules#

Categories switch on many rules at once: correctness, suspicious, pedantic, perf, style, restriction, nursery.

bunfig.toml
[lint.categories]
suspicious = "warn"
perf = "warn"

A category includes the React, Next.js, Jest and Vitest rules only if your package.json lists that package. See lint.categories.

Turn a plugin off#

Use * as the rule name to turn off every rule of a plugin. A setting for a single rule overrides it.

bunfig.toml
[lint.rules]
"jsx-a11y/*" = "off"
"unicorn/*" = "off"
"unicorn/no-null" = "warn"

Ignore files#

bunfig.toml
[lint]
ignorePatterns = ["vendor/", "**/*.gen.ts"]

The patterns use .gitignore syntax and are relative to bunfig.toml. With an eslint.config.* file they're ESLint ignores instead.

ES modules and CommonJS#

bun lint works out whether a file is an ES module or CommonJS from the file itself, the way Node.js does:

  • .mjs and .mts files are modules. .cjs and .cts files are CommonJS.
  • Any other file is a module if the nearest package.json has "type": "module".
  • Otherwise, a file with an import, an export, import.meta or a top-level await is a module. A file with none of them is CommonJS.

A CommonJS file can return at the top level, and it's only in strict mode if it starts with "use strict".

To decide for every file yourself, set sourceType to "module", "commonjs" or "script":

bunfig.toml
[lint.parserOptions]
sourceType = "commonjs"

Next to an eslint.config.js, ESLint's defaults apply instead: .js and .mjs files are modules and .cjs files are CommonJS, unless languageOptions.sourceType says otherwise.

Disable a rule in code#

bun lint uses ESLint's comments, so existing ones keep working. oxlint's oxlint-disable comments mean the same, except next to an ESLint config, where they are ordinary comments as in ESLint.

// eslint-disable-next-line no-console -- the CLI prints its version
console.log(version);

/* eslint-disable no-console */
console.log("off until the end of the file");

A comment that no longer disables anything gets a warning, and --fix removes it.

Type-aware rules#

Some bugs only show up with types, like a promise nobody awaits. --type-aware turns on the recommended rules that use them.

terminal
bun lint --type-aware
5 | export function onClick() {
6 |   save("hello");
      ^
error: Promises must be awaited, end with a call to .catch, ...  @typescript-eslint/no-floating-promises
    at src/save.ts:6:3

Types come from bun check, Bun's built-in type checker. It's slower than a plain run, because your code is type checked first. To make it the default:

bunfig.toml
[lint]
typeAware = true

Only want one of them? Turn it on like any other rule. You don't need typeAware.

bunfig.toml
[lint.rules]
"@typescript-eslint/no-floating-promises" = "error"

To report type errors too, like bun check does, set typeCheck = true or pass --type-check.

Choosing a tsconfig.json#

Each file is type checked with the tsconfig.json your editor would use for it. To use one tsconfig.json for the whole run, pass --tsconfig.

terminal
bun lint --type-aware --tsconfig tsconfig.lint.json

A tsconfig.json without "include" covers every file below it. At the root of a monorepo, that can mean loading the whole repository to lint one package. Bun warns you when that happens.

warn: Loaded 10,114 files in 1.4s to lint vite.config.ts
  ../../tsconfig.json has no "include", so it covers every file under it.
  To speed this up, add "include" to it or pass --tsconfig.

With an oxlint config, --tsconfig does what it does in oxlint. It sets the tsconfig.json that the import rules resolve imports with, for its paths. It doesn't change which tsconfig.json a file is type checked with, so the warning only suggests "include".

Git hooks#

--staged lints only the files you're about to commit, so you don't need lint-staged.

.git/hooks/pre-commit
#!/bin/sh
bun lint --staged --fix

If a file has both staged and unstaged changes, Bun lints the staged version, which is what the commit gets. --fix doesn't write such a file, because staging it again would commit the rest. If it has something to fix, Bun names the file and exits with code 2.

The same goes for the files that a staged file imports, which the type-aware rules and the import rules read. A file you staged and then deleted is still linted. Config files, such as bunfig.toml and eslint.config.js, are read from disk.

--changed lints the files that differ from your default branch. Pass a ref to compare with something else.

terminal
bun lint --changed
bun lint --changed=origin/dev

CI#

In GitHub Actions, bun lint annotates the pull request diff automatically.

.github/workflows/lint.yml
steps:
  - uses: actions/checkout@v4
  - uses: oven-sh/setup-bun@v2
  - run: bun lint --max-warnings 0 # fail on warnings too

For other systems, pick a report format with --format: json, junit, checkstyle, gitlab, sarif, unix.

Migrate from ESLint or oxlint#

Already have an eslint.config.js, .eslintrc, or .oxlintrc.json? bun lint reads it and reports what that tool reports. Change the script and you're done.

package.json
{
  "scripts": {
    "lint": "eslint ." 
    "lint": "bun lint" 
  }
}

If package.json has a lint script, bun lint runs that script. Inside the script, bun lint is the linter. If package.json has "lint" in it but can't be parsed, as in the middle of a merge, bun lint can't tell whether that is a script: it lints nothing and exits with code 2.

Writing a tool that calls Bun in projects you don't control? Pass --no-script: bun lint --no-script is always the linter.

Rules from built-in plugins run natively. Any other ESLint plugin still runs, in JavaScript. To see which is which, and which packages you can uninstall:

terminal
bun lint --doctor

See the guides: Migrate from ESLint and Migrate from oxlint. For custom rules, see ESLint plugins.

CLI Usage#

bun lint [flags] [files, directories, or globs]

Rules#

--rulestring

Set a rule for this run: --rule 'eqeqeq: [error, smart]'. Turn off a plugin: --rule 'unicorn/*: off'

--type-awareboolean

Also run the rules that need type information

--rulesboolean

List every built-in rule. Add -f json for machine-readable output

--explainstring

Print the documentation for a rule

--initboolean

Add a [lint] section to bunfig.toml

Fixing#

--fixboolean

Apply fixes and write the files

--fix-suggestionsboolean

Also apply suggestions, which may change what the code does

--fix-dry-runboolean

Compute fixes without writing them. The json format includes the fixed code

Files#

--stagedboolean

Lint only the files staged in Git

--changedstring

Lint only the files that differ from a Git ref. Defaults to the default branch

--ignore-patternstring

Skip files that match this glob. Can be repeated

--no-ignoreboolean

Lint ignored files too

Output#

-f, --formatstringdefault:pretty

pretty, stylish, json, unix, github, junit, checkstyle, gitlab, sarif, or agent

-o, --output-filestring

Write the report to a file

--quietboolean

Report errors only

--max-warningsnumber

Exit with code 1 if there are more warnings than this

--allboolean

Show every problem. By default, more than 50 identical problems are grouped

Tools#

--doctorboolean

Show which rules of your ESLint or oxlint config run natively, and which packages you can remove

--print-configstring

Print the resolved config for a file

--no-scriptboolean

Run the linter even if the project has a lint script

bun lint also accepts the flags of ESLint and oxlint. Run bun lint --help --all to see them.