Lint and code style
Have agents keep executable format and lint conventions: shared config, EditorConfig, a clear split between formatters and linters, and a deliberate split between pre-commit and CI. This page follows flow → config → split → hooks & CI → governance → lab.
In monorepos or polyglot repos, the SKILL should state root vs package extension to avoid duplicate rules and version drift; separating format from lint reduces tool fights.
Align hooks with CI: auto-fix locally in pre-commit; CI enforces non-auto-fixable rules as hard gates to reduce noisy red builds.
Editor to merge (local →CI)
[ Edit / generate code ]
│
▼
┌─────────────┐ EditorConfig + optional format on save
│ Workspace │──── Shared extends: root .eslintrc / ruff.toml / biome.json
└─────────────┘
│
▼
┌─────────────┐ format + lint --fix; block commit or auto-stage
│ pre-commit │──── Same tool versions as CI (lockfile or npx/pipx pin)
└─────────────┘
│
▼
┌─────────────┐ lint (no fix) / typecheck / tests; paths-ignore artifacts
│ CI gates │──── Don’t redundantly re-run formatter diffs already committed
└─────────────┘
│
▼
[ Merge ]
Principle: editors and hooks should “auto-fix” as much as possible; CI should mostly “report only” or use separate advisory jobs—avoid humans and bots nagging about the same formatting delta in one PR.
Shared config and monorepo extensions
Root holds team baseline packages or extends presets; packages declare deltas (browser vs Node, test overrides). Bump shared rule packages with changelog notes when agents change rules.
- Line endings, indent, charset align with
.editorconfig—don’t fight the formatter. - Multi-runtime (JS + Python): one source of truth per stack—no copy-pasted configs.
- Generated dirs (
dist/,coverage/) ignored in tools and CIpaths-ignore.
Prettier full configuration and EditorConfig example:
// .prettierrc.json — Prettier full configuration
{
"semi": true,
"singleQuote": true,
"quoteProps": "as-needed",
"trailingComma": "all",
"tabWidth": 2,
"useTabs": false,
"printWidth": 100,
"bracketSpacing": true,
"bracketSameLine": false,
"arrowParens": "always",
"endOfLine": "lf",
"embeddedLanguageFormatting": "auto",
"overrides": [
{
"files": ["*.json", "*.yaml", "*.yml"],
"options": { "tabWidth": 2 }
},
{
"files": "*.md",
"options": { "proseWrap": "always", "printWidth": 80 }
}
]
}
// .prettierignore
// dist/
// coverage/
// **/*.generated.ts
// pnpm-lock.yaml
# .editorconfig — aligned with Prettier
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
max_line_length = 80
[Makefile]
indent_style = tab # Makefile requires tabs
Formatting vs lint
Formatters (Prettier / Ruff format / rustfmt)
- Deterministic output; fewer options, fewer debates.
- Don’t stack ESLint stylistic rules on top—pick a primary.
- Large or generated files:
.prettierignoreor equivalents.
Lint (semantics and policy)
- Label auto-fixable vs not; tier severities.
- Avoid disabling whole categories without discussion—narrow with overrides.
- New rules: advisory / report-only first, then enforce with data.
pre-commit vs CI
- Hooks: fast, fixable, reproducible locally; cache deps.
- CI: full matrices, checks you must not skip locally, uploaded reports.
- Don’t leave hooks “warn—and CI “error—indefinitely—document transitions with end dates.
lint-staged + Husky full configuration for pre-commit hooks:
// 1. Install Husky + lint-staged
// npm install -D husky lint-staged
// npx husky init
// package.json — lint-staged config (only lint changed files)
{
"lint-staged": {
"*.{ts,tsx}": [
"eslint --max-warnings 0 --fix",
"prettier --write"
],
"*.{js,jsx,mjs,cjs}": [
"eslint --max-warnings 0 --fix",
"prettier --write"
],
"*.{json,yaml,yml,md}": [
"prettier --write"
],
"*.py": [
"ruff check --fix",
"ruff format"
]
}
}
# .husky/pre-commit — run lint-staged before commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged
# .husky/pre-push — run tests before push (optional, use carefully if slow)
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm run test:unit # unit tests only, not integration tests
// ESLint + Prettier collaboration config (resolve conflicts)
// Solution: eslint-config-prettier disables ESLint rules that conflict with Prettier
// npm install -D eslint-config-prettier
// Add "prettier" at the end of extends in .eslintrc.json (must be last):
{
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"prettier" // must be last, overrides rules conflicting with Prettier
]
}
Rule governance and upgrades
Record contentious choices (quotes, trailing commas) in ADRs or config comments; major ESLint/Ruff upgrades go staged with migration lists (breaking diffs, disables, follow-up issues).
- Post-merge review: rules heavily
eslint-disabled deserve downgrade or narrower scope. - When agents edit shared config, update onboarding “recommended extensions—docs.
Command memo lab (page JS)
Select stacks and surfaces to copy sample package.json scripts and a pre-commit vs CI memo—adjust for your package manager and directories.
Non-Node stacks list equivalent CLI in comment blocks; monorepos should use pnpm -r / turbo run etc. Add generated dirs to tool ignores and CI path filters.
---
name: lint-format
description: Unify lint, format config, and CI auto-fix flows
---
# Tool responsibility separation
- Prettier: formatting (deterministic output, no overlap with ESLint stylistic)
- ESLint: semantics and policy (banned items / deprecated APIs / complexity)
- EditorConfig: editor-level conventions (line endings / indent / charset)
- lint-staged: only lint/format changed files, fast
# Config file checklist
- .prettierrc.json ← Prettier rules (semi/singleQuote/trailingComma etc.)
- .prettierignore ← dist/ coverage/ *.generated.ts
- .eslintrc.json ← extends: [..., "prettier"] (prettier must be last)
- .editorconfig ← aligned with Prettier (lf/2-space/utf-8)
- package.json ← lint-staged config
# Husky setup
- .husky/pre-commit → npx lint-staged
- .husky/pre-push → npm run test:unit (optional)
# lint-staged recommended config
- *.{ts,tsx} → eslint --fix + prettier --write
- *.{json,yaml,md} → prettier --write
- *.py → ruff check --fix + ruff format
# pre-commit vs CI division of labor
- pre-commit: fast, auto-fix (write/fix mode)
- CI: check only (--check/--no-fix), fail = red, no file modifications
- Same rule must not differ in severity between local and CI (local warn vs CI error)
# ESLint + Prettier collaboration
- Install eslint-config-prettier
- Add "prettier" at end of extends (disables conflicting rules)
- Ban simultaneous use of ESLint stylistic rules and Prettier
# Rule governance
- Document contentious rule decisions with ADR or inline comments
- Staged migration when upgrading ESLint/Ruff major versions
- Downgrade or narrow scope of rules heavily eslint-disabled