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.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- eslint-setup Installs to ~/.agents/skills/eslint-setup/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install eslint-setup The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project eslint-setup Installs to .agents/skills/eslint-setup/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/eslint-setup.zipunzip -o eslint-setup.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) eslint-setup PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
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.
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.
| Found | Situation | Path |
|---|---|---|
ESLint 10 + .eslintrc* | Config is silently not used: v10 removed eslintrc | Migrate now |
ESLint 9 + .eslintrc* + ESLINT_USE_FLAT_CONFIG=false | Legacy mode, ESLint 9 is end of life | Migrate, then upgrade to 10 |
| ESLint 8 or older | Unsupported | Upgrade to 10 and write flat config |
eslint.config.* already | Flat config | Audit 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:
| Preset | When |
|---|---|
recommended | No type info available, or lint speed matters more than depth |
recommendedTypeChecked | Default for application code: catches floating promises, unsafe any flows |
strictTypeChecked | Libraries and teams that already pass recommendedTypeChecked |
stylisticTypeChecked | Optional 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 withoutfilesapplies to everything ESLint lints. - ESLint lints
.js,.mjsand.cjsby default. Other extensions (.ts,.vue) are linted only if some object’sfilesmatches them. - An object containing only
ignoresis a global ignore (useglobalIgnores()to make it explicit).ignoresnext to other keys only excludes files from that one object. - Objects merge in order; later objects win for the same rule.
.gitignoreis not read automatically. UseincludeIgnoreFilefrom@eslint/compatif you want it, or list directories inglobalIgnores.- Ignore patterns are relative to the config file:
dist/ignores only thedistdirectory 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:
| Legacy | Flat config |
|---|---|
env: { browser: true } | languageOptions.globals: globals.browser |
.eslintignore | globalIgnores([...]); 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.project | tseslint.configs.* plus parserOptions.projectService: true |
--ext, --env, --rulesdir, --ignore-path flags | files globs, globals, local plugin objects, globalIgnores |
/* eslint-env node */ comments | Reported 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 sureprojectServicefinds 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.allowDefaultProjectwith a short explicit glob list. --inspect-configstarts a local server; it does not need network but cannot open a browser inside a sandbox. Use--print-configthere.
Adopting rules on an existing codebase
Do not turn rules to warn and forget them. Options, in order of preference:
npx eslint . --fixfor auto-fixable rules, committed separately.- Bulk suppressions:
npx eslint . --suppress-all(or--suppress-rule <rule>) records current violations ineslint-suppressions.jsonso only new violations fail;--prune-suppressionsremoves entries once fixed. Commit the file. - Scoped
filesoverrides 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-prettiermust come last, and it only disables rules; it does not run Prettier. Do not addeslint-plugin-prettierunless the team wants formatting errors reported as lint errors (slow and noisy).- Mixing
tseslint.config()anddefineConfig()styles in one file works but confuses readers; standardise ondefineConfigfromeslint/config. - Plugin keys are the namespace in rule names: registering a plugin as
tsmakes the rulets/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.yaml
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.