ESLint Setup skill for Codex

Flat config that lints the files you think it does. Codex writes or migrates eslint.config.mjs with defineConfig, globalIgnores and typescript-eslint projectService, keeps type-aware rules away from files outside tsconfig, and puts eslint-config-prettier last. It checks the merged config per file type, compares violation counts against the old setup to catch files that silently stopped being linted, and adopts strict rules on legacy code with bulk suppressions instead of warnings.

  • Engineering Workflow
  • $eslint-setup
  • 193 lines
  • 2 files
  • ~2,150 tokens
  • MIT
  • Updated
  • Editor's pick

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- eslint-setup

Installs to ~/.agents/skills/eslint-setup/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $eslint-setup, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Configures ESLint with flat config (eslint.config.js) on ESLint 9 and 10 - defineConfig, globalIgnores, typescript-eslint with projectService typed linting, globals, eslint-config-prettier, monorepo config lookup, bulk suppressions and caching - and migrates legacy .eslintrc and .eslintignore setups with @eslint/migrate-config. Use when adding ESLint to a JavaScript or TypeScript project, upgrading to ESLint 10 where eslintrc no longer works, fixing a flat config that ignores files or rules, or speeding up slow type-aware linting.

Use it when

  • Adding ESLint to a JavaScript or TypeScript project
  • Upgrading to ESLint 10, where eslintrc files are no longer read
  • Flat config ignores files, applies the wrong rules, or is slow
  • Turning on stricter typed rules across an existing codebase

Use something else when

  • You want one fast tool for both linting and formatting instead of ESLint plus Prettier: use biome-setup
  • You are wiring lint into git hooks rather than configuring rules: use pre-commit-hooks
  • You are fixing TypeScript compiler errors, not lint findings: use typescript-type-errors

How to check the result

Run npx eslint --print-config on one file of each type to confirm parser and rules, then compare per-rule violation counts with the previous setup and require npx eslint . --max-warnings=0 to pass.

The mistake it prevents

Migrating to flat config without adding .ts patterns to files, so TypeScript is silently skipped and the clean lint run looks like an improvement.

~/.agents/skills/eslint-setup/SKILL.md

--- name: eslint-setup description: Configures ESLint with flat config (eslint.config.js) on ESLint 9 and 10 - defineConfig, globalIgnores, typescript-eslint with projectService typed… license: MIT ---

Done well means one eslint.config.* that lints exactly the intended files, typed rules only where type information exists, no rules that duplicate the formatter, and a clean npx eslint . --max-warnings=0 in CI. Existing violations are fixed or suppressed deliberately, never by disabling rules wholesale.

Inspect first

npx eslint --version
ls eslint.config.* .eslintrc* .eslintignore 2>/dev/null
node -p "require('./package.json').type"     # "module" -> .js is ESM; otherwise use .mjs
cat package.json | grep -nE '"(lint|eslint|prettier)'

TypeScript config files (eslint.config.ts) also work when jiti 2.2+ is installed. Read AGENTS.md for the package manager and lint command. Note whether Prettier or Biome formats the code; ESLint must not fight it.

FoundSituationPath
ESLint 10 + .eslintrc*Config is silently not used: v10 removed eslintrcMigrate now
ESLint 9 + .eslintrc* + ESLINT_USE_FLAT_CONFIG=falseLegacy mode, ESLint 9 is end of lifeMigrate, then upgrade to 10
ESLint 8 or olderUnsupportedUpgrade to 10 and write flat config
eslint.config.* alreadyFlat configAudit with --inspect-config

ESLint 10 requires Node ^20.19.0 || ^22.13.0 || >=24. Check node -v and the CI image.

Install

npm install --save-dev eslint @eslint/js typescript-eslint globals eslint-config-prettier

Needs network; in a Codex sandbox without it, ask for approval. Use the repo’s package manager (pnpm add -D, yarn add -D, bun add -d).

Reference config (TypeScript, Node or browser)

// eslint.config.mjs
// @ts-check
import js from "@eslint/js";
import { defineConfig, globalIgnores } from "eslint/config";
import tseslint from "typescript-eslint";
import globals from "globals";
import prettier from "eslint-config-prettier/flat";

export default defineConfig([
  globalIgnores(["dist/", "coverage/", "**/generated/", "**/*.min.js"]),

  {
    files: ["**/*.{js,mjs,cjs,ts,tsx,mts,cts}"],
    extends: [js.configs.recommended, tseslint.configs.recommendedTypeChecked],
    languageOptions: {
      globals: { ...globals.node },          // or globals.browser
      parserOptions: {
        projectService: true,
        tsconfigRootDir: import.meta.dirname,
      },
    },
    linterOptions: { reportUnusedDisableDirectives: "error" },
    rules: {
      "@typescript-eslint/no-floating-promises": "error",
      "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
    },
  },

  // Plain JS config files are outside tsconfig: no type-aware rules there.
  { files: ["**/*.{js,mjs,cjs}"], extends: [tseslint.configs.disableTypeChecked] },

  // Tests: relax rules that fight test idioms.
  {
    files: ["**/*.test.{ts,tsx}", "tests/**"],
    rules: { "@typescript-eslint/no-non-null-assertion": "off" },
  },

  prettier, // last: turns off stylistic rules that conflict with the formatter
]);

Choosing the typescript-eslint preset:

PresetWhen
recommendedNo type info available, or lint speed matters more than depth
recommendedTypeCheckedDefault for application code: catches floating promises, unsafe any flows
strictTypeCheckedLibraries and teams that already pass recommendedTypeChecked
stylisticTypeCheckedOptional add-on; skip if a formatter plus code review already covers style

How flat config resolves (the rules that cause most bugs)

  • Every config object applies to the files matched by its files; an object without files applies to everything ESLint lints.
  • ESLint lints .js, .mjs and .cjs by default. Other extensions (.ts, .vue) are linted only if some object’s files matches them.
  • An object containing only ignores is a global ignore (use globalIgnores() to make it explicit). ignores next to other keys only excludes files from that one object.
  • Objects merge in order; later objects win for the same rule.
  • .gitignore is not read automatically. Use includeIgnoreFile from @eslint/compat if you want it, or list directories in globalIgnores.
  • Ignore patterns are relative to the config file: dist/ ignores only the dist directory next to it; use **/dist/ to cover nested packages.
  • ESLint 10 looks up the config from each linted file’s directory upward, so nested eslint.config.* files in packages of a monorepo now apply to their own files.

Migrating from .eslintrc

npx @eslint/migrate-config .eslintrc.json      # writes eslint.config.mjs; also handles .eslintrc.yml/.cjs

Then fix what the tool cannot judge:

LegacyFlat config
env: { browser: true }languageOptions.globals: globals.browser
.eslintignoreglobalIgnores([...]); delete the file (v10 ignores it)
overrides: [{ files, rules }]Separate objects with files
extends: "plugin:x/recommended"Import the plugin and use its flat preset; FlatCompat from @eslint/eslintrc only for plugins with no flat export
parser + parserOptions.projecttseslint.configs.* plus parserOptions.projectService: true
--ext, --env, --rulesdir, --ignore-path flagsfiles globs, globals, local plugin objects, globalIgnores
/* eslint-env node */ commentsReported as errors in v10: delete them and set globals in config

Verify parity before deleting the old config: run the old setup on the previous ESLint version and the new one on the same commit, and compare counts per rule (npx eslint . -f json | node -e "..." or --format stylish | tail). A large drop usually means files are no longer matched, not that the code got better.

Debugging and speed

npx eslint --inspect-config                     # browser UI showing which objects apply to a file
npx eslint --print-config src/app.ts            # final merged config for one file
TIMING=1 npx eslint .                           # slowest rules
npx eslint . --cache --cache-location node_modules/.cache/eslint/
npx eslint . --debug 2>&1 | grep -i "ignored"   # why a file is skipped
  • Typed linting is the usual cost. Restrict type-checked presets to src/** if tests or scripts are not in a tsconfig, and make sure projectService finds a tsconfig that includes every linted TS file (otherwise “file was not found by the project service”).
  • For files deliberately outside any tsconfig, use parserOptions.projectService.allowDefaultProject with a short explicit glob list.
  • --inspect-config starts a local server; it does not need network but cannot open a browser inside a sandbox. Use --print-config there.

Adopting rules on an existing codebase

Do not turn rules to warn and forget them. Options, in order of preference:

  1. npx eslint . --fix for auto-fixable rules, committed separately.
  2. Bulk suppressions: npx eslint . --suppress-all (or --suppress-rule <rule>) records current violations in eslint-suppressions.json so only new violations fail; --prune-suppressions removes entries once fixed. Commit the file.
  3. Scoped files overrides for legacy directories, with a ticket to remove them.

Inline // eslint-disable-next-line rule -- reason only with a reason; with reportUnusedDisableDirectives: "error" stale ones fail the build.

Workflow

- [ ] 1. Inspect version, configs, module type, formatter
- [ ] 2. Install or upgrade packages (approval if network needed)
- [ ] 3. Write or migrate eslint.config.mjs
- [ ] 4. --print-config on one file per file type; confirm parser and rules
- [ ] 5. Run on the whole repo; compare counts with the previous setup
- [ ] 6. --fix in its own commit; suppress or scope the remainder
- [ ] 7. Scripts: "lint": "eslint . --max-warnings=0 --cache"; wire into CI and hooks

Gotchas

  • eslint-config-prettier must come last, and it only disables rules; it does not run Prettier. Do not add eslint-plugin-prettier unless the team wants formatting errors reported as lint errors (slow and noisy).
  • Mixing tseslint.config() and defineConfig() styles in one file works but confuses readers; standardise on defineConfig from eslint/config.
  • Plugin keys are the namespace in rule names: registering a plugin as ts makes the rule ts/no-explicit-any, and every override must use that prefix.
  • Upgrading ESLint major without upgrading plugins produces confusing “rule not found” or crash errors; check each plugin’s peer range.

Report: ESLint and plugin versions, files linted (count before and after), rule violation counts, what was auto-fixed, what was suppressed and why.

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "ESLint Setup"
  short_description: "Flat config that lints the files you think it does"
  default_prompt: "$eslint-setup Migrate our .eslintrc.json and .eslintignore to flat config so we can upgrade to ESLint 10"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
New

Introducing Codex Skills Pro

79 professional skills that take Codex through complete engagements, each with its own scripts, references and a report that verifies the result.

All-AccessOne-time payment, no subscription
$49
Get All-AccessOr browse individual skills from $2.99 →

Secure checkout by Creem