| name | setup-lint-format-precommit |
| description | Stands up a lint, format, and pre-commit toolchain (Biome or ESLint flat config + Prettier, or ruff + ruff format) with .editorconfig, fast staged-only git hooks (husky+lint-staged or the pre-commit framework), and a check-only CI gate (lint --max-warnings=0 + format --check, no auto-fix), plus a one-shot reformat of a dirty repo hidden behind .git-blame-ignore-revs. |
| when_to_use | A repo has no/inconsistent linting or formatting, style churn floods diffs, or commits bypass checks and you're adding enforced gates. Distinct from type-safety-strict (type-checker strictness, not style), code-review (human correctness review), refactor-cleanup (behavior-preserving cleanups, not gates), and pin-toolchain-versions (pinning the binaries the gate runs). |
When to Use
- "Set up ESLint/Biome/ruff + a formatter for this repo"
- "Add a pre-commit hook so unformatted/lint-failing code can't get committed"
- "Whitespace/quote/import-order churn is polluting every diff — kill it"
- "CI should fail on lint warnings and unformatted files"
- "We have config but it's slow, inconsistent across machines, or people
--no-verify past it"
NOT this skill:
- Making the type checker strict (
strict: true, removing any, mypy --strict) → type-safety-strict
- Judging whether the code is correct (logic bugs, edge cases) → code-review
- Behavior-preserving cleanups/renames/dedup of working code → refactor-cleanup
- Pinning Node/Python/tool versions so everyone runs the same binaries → pin-toolchain-versions
- A monorepo's cross-package task wiring/caching/affected-only runs → setup-monorepo-tooling
Steps
-
Pick the toolchain by ecosystem — never run a linter and a formatter that fight over the same rules. The linter checks logic/correctness rules; the formatter owns whitespace/quotes/commas. Never leave ESLint stylistic/formatting rules on alongside Prettier.
| Stack | Default | Why |
|---|
| JS/TS, want speed + one tool | Biome (biome lint + biome format) | One Rust binary, no plugin graph, ~10–100x faster, lint+format+import-sort in one config |
| JS/TS, need plugin ecosystem (React, a11y, import, custom) | ESLint flat config (eslint.config.js) + Prettier | Plugin coverage Biome lacks; Prettier owns formatting so disable ESLint stylistic rules |
| Python | ruff (ruff check) + ruff format | Replaces flake8+isort+black+pyupgrade in one tool; ruff format is black-compatible |
Default to Biome for greenfield JS/TS; switch to ESLint flat + Prettier only when a required plugin (e.g. eslint-plugin-jsx-a11y, eslint-plugin-import) has no Biome equivalent; ruff + ruff format for Python. Don't reach for ESLint legacy .eslintrc — flat config is the only supported format on ESLint v9+.
-
Write minimal config that extends a shared base — don't hand-roll a 200-rule file. Turn on the recommended preset, override only the handful you actually disagree with.
Biome (biome.json):
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"vcs": { "enabled": true, "clientKind"
Common Errors
- Linter and formatter fighting. ESLint stylistic rules (or
airbnb quote/semi rules) vs Prettier produce an infinite "fixed by one, broken by the other" loop. Add eslint-config-prettier last in the flat config to disable every conflicting rule; let the formatter own formatting.
- Hook lints the whole repo. A pre-commit that runs
eslint . takes 30s+ and gets --no-verify'd into uselessness. Use lint-staged / pre-commit's built-in file filtering so it only touches staged paths.
- CI auto-fixes instead of checking.
eslint --fix / prettier --write / ruff --fix in CI either commits unreviewed changes or, on a read-only checkout, masks failures. CI must use --check/ci/--max-warnings=0 and fail, not mutate.
- Warnings allowed in CI. Without
--max-warnings=0, warnings accumulate into noise nobody reads. Treat warnings as errors in CI; downgrade a rule to off deliberately if you truly don't want it.
- Format sweep mixed into a feature PR. Reviewers can't see the real diff and
git blame points everything at you. Reformat in its own commit and register it in .git-blame-ignore-revs.
- No
.editorconfig / format-on-save. Editors keep reintroducing CRLF/tabs/trailing whitespace, so the hook fires on every commit. Fix it at the editor with a committed .editorconfig + formatOnSave.
- Legacy
.eslintrc with new ESLint. ESLint v9 defaults to flat config; a leftover .eslintrc.json is silently ignored or errors. Migrate to eslint.config.js.
- Unpinned hook/tool versions.
pre-commit autoupdate or a floating biome/eslint makes CI and local disagree and breaks reproducibly-later. Pin hook revs and lock tool versions (a lockfile, or coordinate with pin-toolchain-versions).
- Ignoring generated/vendored dirs not configured. Linting
dist/, build/, coverage/, .next/, migrations, or snapshots floods output and slows everything. Set ignores in config (and /-equivalent) so they're skipped everywhere — hook and CI alike.
Verify
- Bad file is caught by the formatter check: create a deliberately mangled file (wrong indent, double→single quotes, no final newline).
biome ci . / prettier --check . / ruff format --check . exits non-zero and names that file.
- Bad file is caught by the linter: add an unused import /
== where a rule forbids it. eslint . --max-warnings=0 / biome lint . / ruff check . exits non-zero.
- Hook blocks the commit:
git add the bad file and git commit — the commit is rejected (or the file is auto-fixed and you must re-stage), proving the hook runs on staged files.
- Hook is fast: time a commit touching one file — pre-commit completes in a few seconds, not tens (proves it's staged-only, not whole-repo).
- CI gate fails on drift: push the bad file (or run the CI command locally) → the lint/format job is red; fix it → green. Confirm CI uses
--check/--max-warnings=0, never --write/--fix.
- Clean baseline: on a freshly formatted tree, the full CI command exits 0 with no changes — the reformat commit landed and is in
.git-blame-ignore-revs (git blame skips it).
Done = a deliberately bad file is rejected by both the local pre-commit hook (in seconds, staged-only) and the CI gate (lint --max-warnings=0 + format --check, no auto-fix), the formatter and linter don't fight, and the existing tree is already clean behind a single blame-ignored reformat commit.