Ruff Setup skill for Codex
One pinned Ruff in place of flake8, Black and isort. Codex configures Ruff in pyproject.toml with a deliberate rule tier rather than the bare defaults or ALL, leaves out rules that conflict with the formatter, and translates flake8 plugins, isort profiles and Black options using a bundled mapping table. Before and after counts per rule code prove nothing was lost, mass fixes land as separate commits, and the same pinned version runs in pre-commit and CI.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- ruff-setup Installs to ~/.agents/skills/ruff-setup/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install ruff-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 ruff-setup Installs to .agents/skills/ruff-setup/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/ruff-setup.zipunzip -o ruff-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))) ruff-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 $ruff-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 Ruff as the Python linter and formatter in pyproject.toml or ruff.toml - rule selection beyond the defaults, per-file-ignores, isort settings, formatter options that avoid lint conflicts, pinned versions - and migrates projects off flake8 and its plugins, Black, isort, pyupgrade and autoflake with a measured baseline and noqa backfill. Use when adding Ruff to a Python project, replacing flake8 or Black with Ruff, choosing which Ruff rules to enable, or fixing Ruff results that differ between local runs, pre-commit and CI.
Use it when
- Adding linting and formatting to a Python project
- Migrating from flake8 and its plugins, Black, isort or pyupgrade
- Deciding which Ruff rule prefixes to enable
- Ruff results differ between editor, pre-commit and CI
Use something else when
- You need static type checking with mypy or pyright: use python-typing
- You are only wiring hooks for already-configured tools: use pre-commit-hooks
- The project is JavaScript or TypeScript: use biome-setup
How to check the result
ruff check --statistics shows only expected codes with counts explained against the old flake8 baseline, and ruff check plus ruff format --check pass in CI with the same pinned version.
The mistake it prevents
Using exclude instead of extend-exclude, which wipes Ruff's default exclusions and starts linting .venv and build directories.
Done well means one Ruff version pinned across local, pre-commit and CI; a rule
selection chosen on purpose (not ALL, not just the defaults); no rules that fight the
formatter; and ruff check plus ruff format --check both clean in CI, with any legacy
violations either fixed or recorded as explicit noqa comments.
Inspect
ls pyproject.toml ruff.toml .ruff.toml setup.cfg .flake8 tox.ini .isort.cfg .pre-commit-config.yaml 2>/dev/null
grep -nA30 '^\[tool\.\(ruff\|black\|isort\|flake8\)' pyproject.toml
grep -nE 'ruff|flake8|black|isort|pyupgrade|autoflake|pylint' requirements*.txt pyproject.toml .pre-commit-config.yaml 2>/dev/null
Read AGENTS.md for the dependency manager (uv, poetry, pip-tools) and check commands.
Note project.requires-python; Ruff infers target-version from it.
Install
uv add --dev ruff==0.17.3 # or: poetry add --group dev [email protected] ; pip install ruff==0.17.3
Use the current release at install time and pin it exactly: new Ruff versions add rules and promote preview behaviour, which can turn CI red on unrelated PRs. Installing needs network; in an offline Codex sandbox ask for approval or use a Ruff already in the venv.
Reference configuration
# pyproject.toml
[tool.ruff]
line-length = 100
extend-exclude = ["migrations/versions", "src/generated"]
# target-version comes from project.requires-python; set it only if that is missing
[tool.ruff.lint]
select = [
"E4", "E7", "E9", "F", # Ruff defaults: real errors only
"I", # isort
"B", # bugbear: likely bugs
"UP", # pyupgrade for the target version
"SIM", # simplifications
"C4", # comprehensions
"PT", # pytest style
"RUF", # Ruff-specific, includes unused-noqa
]
ignore = ["SIM108"] # ternary rewrite: taste, often less readable
[tool.ruff.lint.per-file-ignores]
"tests/**" = ["S101", "PLR2004"]
"__init__.py" = ["F401"]
"scripts/**" = ["T20"]
[tool.ruff.lint.isort]
known-first-party = ["myapp"]
[tool.ruff.format]
docstring-code-format = true
Choosing rules
| Tier | Prefixes | Use when |
|---|---|---|
| Baseline | E4 E7 E9 F (default) | Never less than this |
| Recommended for most code | + I B UP SIM C4 RUF | Default choice for applications and libraries |
| Domain add-ons | PT (pytest), DJ (Django), FAST (FastAPI), ASYNC (asyncio), S (bandit security), DTZ (timezones), PD/NPY (data) | The project uses that domain |
| Library polish | D with a convention, N, PL*, RET, PTH, TC | Public libraries with a mature codebase |
| Avoid | ALL | Every upgrade silently enables new rules; also selects conflicting pairs |
Leave off with ruff format | W191 E111 E114 E117 D206 D300 Q000-Q003 COM812 COM819, and ISC001/ISC002 if Ruff warns | They conflict with the formatter; Ruff prints a warning when they are on |
Explain each non-default prefix in a comment, as above. For any single code:
ruff rule B008 prints its rationale and fix behaviour offline.
Migrating from flake8, Black and isort
For the plugin-to-prefix table and settings translation (max-complexity, isort profile, Black string normalisation), read references/flake8-plugin-map.md when the project has an existing flake8, isort, Black or pylint configuration.
- [ ] 1. Record baseline: current tool versions, violation counts per code
- [ ] 2. Translate settings and plugins to [tool.ruff] (reference file)
- [ ] 3. ruff check --statistics: compare counts with the old flake8 run
- [ ] 4. Formatter: ruff format --diff; review differences from Black
- [ ] 5. Commit config; commit safe autofixes; commit the reformat separately
- [ ] 6. Backfill noqa for what remains (or fix it) and enable RUF100
- [ ] 7. Replace old tools in pre-commit, CI, dev deps and editor settings
- [ ] 8. Add mass-change SHAs to .git-blame-ignore-revs
Commands for each step:
flake8 --statistics -qq . | sort -rn | head -30 # before (old tool still installed)
ruff check --statistics . # after: compare codes and counts
ruff check --fix . # safe fixes only
ruff check --fix --unsafe-fixes --diff . # preview unsafe fixes; apply only after review
ruff format --diff . | head -100 # Black -> Ruff formatter differences
ruff check --add-noqa . # baseline remaining violations as noqa comments
ruff check --extend-select RUF100 --fix . # later: remove noqa that no longer apply
- Ruff’s formatter is designed as a drop-in for Black and differs on a small number of edge cases. Expect a handful of changed files, not hundreds; hundreds means a setting (line length, quote style, magic trailing comma) was not carried over.
ruff formatdoes not sort imports. KeepIselected and runruff check --fixbeforeruff format.--add-noqais a debt ledger, not a fix. Prefer fixing when counts are small; use noqa backfill when a large codebase adopts new rules, and tell the user how many lines it touched.
Running everywhere the same way
ruff check . && ruff format --check . # CI gate
ruff check --output-format=github . # GitHub Actions annotations
ruff check --show-settings src/myapp/api.py | head -50 # resolved settings for one file
ruff check --show-files . | head # which files Ruff will lint
pre-commit (lint with --fix before format):
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.17.3 # same version as the dev dependency
hooks:
- id: ruff-check
args: [--fix]
- id: ruff-format
Editor: the Ruff VS Code extension uses the project’s ruff from the environment when it
can; set it as the Python formatter and disable Black, isort and flake8 extensions so
they do not compete.
Configuration discovery gotchas
- Ruff uses the closest
pyproject.toml(with a[tool.ruff]section),ruff.tomlor.ruff.tomlto each file. A nested config replaces the parent entirely unless it saysextend = "../pyproject.toml". In monorepos, give packagesextendor keep one root config. excludereplaces Ruff’s default exclusions (.venv,build,node_modules, and so on); useextend-excludeinstead.- Files passed explicitly on the command line are linted even if excluded, unless
force-exclude = true. Set it when pre-commit or editors pass paths directly. per-file-ignoreskeys are globs relative to the config file’s directory.selectin a nested config overrides,extend-selectadds. Mixing them up silently drops rules.
Verify
ruff check --statistics .shows only the codes you expect, with counts you can explain against the baseline.ruff check . && ruff format --check .exit 0 locally and in CI.pre-commit run --all-filesproduces no changes on a clean tree.- The Ruff version is identical in the lockfile, pre-commit
revand CI.
Report: Ruff version, selected prefixes with one-line reasons, before/after violation counts, files reformatted, number of noqa comments added, old tools removed.
What the zip contains
Codex reads bundled files only when a step points to them. Every one is shown here, so you can review it before installing.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Ruff Setup"
short_description: "One pinned Ruff in place of flake8, Black and isort"
default_prompt: "$ruff-setup Replace flake8, isort and Black with Ruff in this project without losing any checks" references/flake8-plugin-map.md
# Mapping existing Python tooling to Ruff
Read this while migrating, to translate an existing flake8 / isort / Black / pylint setup
into `select` codes and settings. Confirm any code with `ruff rule <CODE>` or
`ruff linter`, since prefixes occasionally change between Ruff releases (for example
flake8-type-checking moved from `TCH` to `TC`).
## Tools and flake8 plugins to rule prefixes
| Existing tool or plugin | Ruff prefix | Notes |
|---|---|---|
| pycodestyle | `E`, `W` | Ruff's default selects only `E4`, `E7`, `E9`; whitespace rules overlap with the formatter |
| pyflakes | `F` | Default-on |
| autoflake | `F401`, `F841` with `--fix` | Unused imports and variables |
| mccabe | `C90` | Threshold via `lint.mccabe.max-complexity` |
| isort | `I` | Run with `ruff check --select I --fix`; `ruff format` does not sort imports |
| pyupgrade | `UP` | Respects `target-version` / `requires-python` |
| flake8-bugbear | `B` | High value, low noise; add to every project |
| flake8-comprehensions | `C4` | |
| flake8-simplify | `SIM` | Some suggestions are taste; ignore individual codes freely |
| flake8-bandit | `S` | Ignore `S101` (assert) under tests |
| flake8-builtins | `A` | |
| flake8-print | `T20` | Good for libraries and services; skip for CLIs that print by design |
| flake8-debugger | `T10` | |
| flake8-pytest-style | `PT` | |
| flake8-quotes | `Q` | Conflicts with the formatter; leave off when using `ruff format` |
| flake8-commas | `COM` | `COM812` conflicts with the formatter; leave off |
| flake8-implicit-str-concat | `ISC` | `ISC001` may conflict with the formatter; Ruff warns if so |
| flake8-annotations | `ANN` | Usually redundant when mypy or pyright enforce annotations |
| flake8-type-checking | `TC` | Formerly `TCH` |
| flake8-tidy-imports | `TID` | Banned imports via `lint.flake8-tidy-imports.banned-api` |
| flake8-import-conventions | `ICN` | `import numpy as np` style aliases |
| flake8-use-pathlib | `PTH` | |
| flake8-return | `RET` | |
| flake8-unused-arguments | `ARG` | |
| flake8-datetimez | `DTZ` | Naive datetime detection |
| flake8-errmsg | `EM` | |
| flake8-logging-format | `G` | |
| flake8-logging | `LOG` | |
| flake8-pie | `PIE` | |
| flake8-raise | `RSE` | |
| flake8-self | `SLF` | |
| flake8-boolean-trap | `FBT` | Noisy on existing code |
| flake8-async | `ASYNC` | |
| flake8-blind-except | `BLE` | |
| flake8-no-pep420 | `INP` | Missing `__init__.py` |
| flake8-2020 | `YTT` | |
| flake8-future-annotations | `FA` | |
| flake8-django | `DJ` | |
| flake8-slots | `SLOT` | |
| flake8-gettext | `INT` | |
| pep8-naming | `N` | |
| pydocstyle | `D` | Pick a convention: `lint.pydocstyle.convention = "google"` (or `numpy`, `pep257`) |
| pydoclint / darglint | `DOC` | Partial coverage; check `ruff linter` for current status |
| eradicate | `ERA` | Commented-out code |
| pandas-vet | `PD` | |
| NumPy-specific | `NPY` | |
| pylint | `PLC`, `PLE`, `PLR`, `PLW` | Subset of pylint; no cross-module inference |
| tryceratops | `TRY` | |
| flynt | `FLY` | |
| perflint | `PERF` | |
| refurb | `FURB` | |
| FastAPI | `FAST` | |
| Airflow | `AIR` | |
| Ruff-specific | `RUF` | Includes `RUF100` (unused `noqa`) |
Plugins with no Ruff equivalent (check before promising parity): wemake-python-styleguide,
flake8-spellcheck, flake8-cognitive-complexity, custom in-house flake8 plugins. Either
keep flake8 running only those plugins (`flake8 --select=<their codes>`) or drop them with
the user's agreement.
## Settings translation
| Old setting | Ruff setting |
|---|---|
| flake8 `max-line-length` / Black `line-length` | `line-length` (top level `[tool.ruff]`) |
| flake8 `max-complexity` | `[tool.ruff.lint.mccabe] max-complexity` |
| flake8 `extend-ignore`, `ignore` | `[tool.ruff.lint] ignore` |
| flake8 `select`, `extend-select` | `[tool.ruff.lint] select`, `extend-select` |
| flake8 `per-file-ignores` | `[tool.ruff.lint.per-file-ignores]` with glob keys |
| flake8 `exclude` | `extend-exclude` (keeps Ruff's defaults such as `.venv`) |
| isort `profile = "black"` | Default behaviour is Black-compatible; nothing to set |
| isort `known_first_party` | `[tool.ruff.lint.isort] known-first-party` |
| isort `force_single_line` | `[tool.ruff.lint.isort] force-single-line = true` |
| isort `combine_as_imports` | `[tool.ruff.lint.isort] combine-as-imports = true` |
| Black `skip-string-normalization` | `[tool.ruff.format] quote-style = "preserve"` |
| Black `skip-magic-trailing-comma` | `[tool.ruff.format] skip-magic-trailing-comma = true` |
| Black `target-version` | `target-version`, or let Ruff read `project.requires-python` |
| pylint `max-args` | `[tool.ruff.lint.pylint] max-args` |
| pylint `disable=` comments | `# noqa: PLR0913` style comments (pylint comments are ignored) |
## Inline suppressions
- flake8 `# noqa: E501` comments work unchanged in Ruff.
- File-level: `# ruff: noqa: F401` (a bare `# flake8: noqa` is also honoured).
- `# type: ignore` and `# pylint: disable` are not Ruff suppressions.
- After migration, enable `RUF100` once to delete `noqa` comments that no longer
suppress anything: `ruff check --extend-select RUF100 --fix`.
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.