{
  "markdown": "# gimme-the-lint\n\n[![npm version](https://img.shields.io/npm/v/@theglitchking/gimme-the-lint.svg)](https://www.npmjs.com/package/@theglitchking/gimme-the-lint)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n[![GitHub Action](https://github.com/TheGlitchKing/gimme-the-lint/workflows/Progressive%20Lint/badge.svg)](https://github.com/TheGlitchKing/gimme-the-lint/actions)\n\n---\n\n## Summary\n\nMost projects use linters to catch mistakes and keep code clean. The problem is\nthat when you add a linter to a project that already has a lot of code, the\nlinter finds hundreds — sometimes thousands — of old problems. Turning linting\non would block every commit until someone fixes all of it, so teams just never\ndo it. **gimme-the-lint** solves this by remembering the old problems and only\nblocking your work when you create a _new_ one. Your team cleans up the old\nstuff at its own pace; meanwhile no new mess gets in.\n\n**v2.0** generalized that idea to any linter and any language — a progressive-lint\n**engine** with a pluggable adapter per tool, across JavaScript/TypeScript, Python,\nGo, Rust, Terraform and Ansible, in any monorepo shape.\n\n**v2.6** applies the same bargain to a different question. Not *\"is this code\nwell-formed?\"* but ***\"does your data model agree with the schemas that expose it?\"***\n\nBecause that is where the expensive bugs live, and they are all silent:\n\n> A user fills in twelve fields. Four are saved. The API returns **201**.\n>\n> Nobody clicks Save on a project and expects every line item to be reset to\n> `pending` and its notes wiped. The endpoint returned **200**.\n>\n> A column is typed `str` in the response and `JSON` in the database. Harmless —\n> until the first *correct* value is written, and every read 500s.\n\nNone of that is a lint error. Nothing is malformed, nothing crashes, and the response\nsays success. **gimme-the-lint now catches it before it ships**, on the same\n\"only-new-violations-block\" terms as everything else — with one exception: a bug that is\n*already breaking production* is never grandfathered.\n\n---\n\n## How it works\n\ngimme-the-lint creates **baselines** — snapshots of every existing violation,\nstored per app under `.gtl/`. On each run it lints, then asks one question of\nevery violation: _is this new, or was it already baselined?_ Only new violations\nblock.\n\nThe trick is the **fingerprint**. Each violation is identified by\n`file + rule + message` — deliberately **not** by line number. So a baselined\nviolation survives code moving up or down a file; only a genuinely new problem\nis ever flagged. (In v1 this job was outsourced to the third-party\n`lint-to-the-future`; v2 owns it, which is what makes every linter equal.)\n\nEach app is bound to the linters its package manifest implies — `package.json`\n→ ESLint, `pyproject.toml` → Ruff, `go.mod` → golangci-lint, `Cargo.toml` →\nClippy, `biome.json` → Biome, `ansible.cfg` / `galaxy.yml` → ansible-lint.\nTerraform has no manifest, so a directory of `*.tf` / `*.tofu` files binds to\ntflint by extension. Drift detection runs per app, so a config or linter-version\nchange in one app never churns the baselines of another.\n\n---\n\n## Features\n\n- **Progressive linting** — only new violations block; existing ones are baselined\n- **In-house diff engine** — line/column-independent fingerprints survive code shifts\n- **Pluggable linter adapters** — the choice of linter is config, not a hardcode\n- **Polyglot** — JavaScript/TypeScript, Python, Go, Rust, Terraform, Ansible out of the box\n- **Per-app model** — auto-discovers every package in a monorepo; no `frontend/`\n  + `backend/` assumption\n- **Per-app drift detection** — app add/remove, config change, linter version, age\n- **Best-practice configs shipped** — `install` seeds each app with a curated,\n  security-aware config for its linter (create-if-absent — never clobbers yours)\n- **Security linting built in** — gitleaks for secrets across every codebase,\n  plus per-language security rules (gosec, Ruff `S`, eslint-plugin-security)\n- **Idempotent skips** — an app with code but no installed linter is warn-skipped\n  (never blocks) — or fails loudly under `--strict`\n- **Offline install** — air-gapped mode for regulated environments\n- **Greenfield mode** — \"strict from day one\" with empty baselines\n- **Git hooks, CLI, GitHub Action, Claude Code plugin** — one engine, four front doors\n- **LLM-optimized output** — failures tell Claude Code to auto-fix without asking\n\n## Supported linters\n\n| Language | Linter | Bound by |\n|----------|--------|----------|\n| JavaScript / TypeScript | `eslint` | `package.json` |\n| JavaScript / TypeScript | `biome` | `biome.json` (supersedes ESLint) |\n| Python | `ruff` | `pyproject.toml`, `requirements.txt`, `setup.py` |\n| Go | `golangci-lint` | `go.mod` |\n| Rust | `clippy` (`cargo clippy`) | `Cargo.toml` |\n| Terraform / OpenTofu | `tflint` | `*.tf` / `*.tofu` files (no manifest) |\n| Ansible | `ansible-lint` | `ansible.cfg`, `galaxy.yml` |\n| SQL migrations | `squawk` | a `migrations/` directory (any language) |\n| Protobuf | `buf`, `buf-breaking` | `*.proto` files |\n| OpenAPI / AsyncAPI | `spectral` | `openapi.yaml`, `asyncapi.yaml` |\n| TypeScript (types) | `tsc` | `tsconfig.json` |\n| Python (types) | `mypy` | a mypy config (`mypy.ini` or `[tool.mypy]`) |\n\n### Type checkers (v2.8)\n\n`tsc` and `mypy` are the two biggest static-analysis surfaces most repos have, and the\nclassic \"we can't turn it on, there are 800 existing errors\" problem — which is exactly\nwhat a baseline is for.\n\nThey differ from every other adapter in one way: **they ignore the staged-file list and\nalways check the whole program.** They have to. The type of an expression in one file\ndepends on declarations in another, so a change in `a.ts` produces errors in `b.ts` —\nfiles your commit never touched. Checked per-file, that breakage reports clean and\nreaches the base branch with a green tick. Checked whole-program, it blocks.\n\nThat costs seconds rather than milliseconds, so both run at **push**, not on every commit.\n\nTheir violations are also identified differently. A type checker *names the types* in its\nmessage — `Argument of type 'Prospect' is not assignable to parameter of type 'Lead'` — so\nrenaming a type rewrites hundreds of messages at once. Keyed on the message, a pure rename\nwould retire every baselined fingerprint and introduce an equal number of new ones,\nblocking a refactor that changed no behavior. So identity is the *shape* of the error with\nthe type names redacted, which survives the rename while still telling two different errors\napart.\n\n## Contract checks (v2.6)\n\nBeyond \"is this code well-formed\": **does your data model agree with the schemas that\nexpose it?**\n\n| Check | Asks | Runs on |\n|-------|------|---------|\n| `contract` | Does every column your model has actually reach a client — and come back? | **push** |\n| `openapi` | Does your published API contract still describe what your code serves? | **push** |\n| `codegen-drift` | Do your frontend's types still match the API they're typed against? | commit |\n| `squawk` | Will this migration take a table-locking hold on production? | commit |\n| `buf-breaking` | Did this commit break somebody's protobuf client? | push |\n| `alembic-check` | Did you change a model and forget to generate a migration? | **CI only** |\n\n### The whole chain, guarded\n\n```\n   Postgres columns\n        ⇕   alembic-check\n   SQLAlchemy models\n        ⇕   17 contract rules\n   Pydantic schemas\n        ↓   materialize → openapi.json\n   openapi.json\n        ↓   openapi-typescript → api-types.ts\n   frontend types\n```\n\n**Drift lives wherever two artifacts must _agree_. It cannot exist where one is\n_derived_.** So the bottom two rungs are derived, and the class of bug that lived there is\nnow inexpressible rather than merely tested-for.\n\nThe bug that motivated it: a component read `prospect.zip`; the API returns `zip_code`.\nBackend correct, contract check green, lockfile fresh, database row right — and a user saw\na blank field for a full release cycle, because `undefined` renders as nothing, and\nnothing looks exactly like data that was never saved.\n\nSeventeen contract rules, **each one standing on a specific production bug** — the\nincident is recorded with the rule and printed by `gtl-contract rules`. A rule whose\nreason is written down is a rule nobody deletes in a hurry.\n\n```bash\ngimme-the-lint materialize   # write down the API contract FastAPI only computes at runtime\ngimme-the-lint verify        # the checks that need a database (CI only — never a git hook)\n```\n\n**Debt is grandfathered; defects are not.** A missing column on a write schema is debt —\nbaseline it, fix it at your own pace. A response field that 500s every read is a\n**defect**: it cannot be baselined, because grandfathering it means writing down *\"we\naccept that this endpoint is broken.\"* You can still except it — in config, with a\nmandatory reason. The friction is the feature.\n\n> ⚠️ **The contract check imports your application.** Run it somewhere that can already\n> import your app — **your test job, not your lint job.** It needs your app's full runtime\n> dependencies and its import-time environment variables. Dropped into a typical `lint`\n> job it will *skip*, and a skip is not a pass: you get a green tick over a check that\n> never ran. `gimme-the-lint install` does not install it for this reason — the venv it\n> creates has ruff and mypy, not your app. See\n> [Where the contract check can run](.documentation/api/contract-guide.md#where-the-contract-check-can-run).\n\nStart with [`.documentation/api/contract-guide.md`](.documentation/api/contract-guide.md).\n\n## Shipped lint configs\n\n`install` seeds every discovered app with a best-practice (\"recommended\" tier)\nconfig for its linter — **created only if absent**, so your own config is never\noverwritten. Each ships a sensible default rule set and a single **lever** to\ndial strictness up or down:\n\n| Codebase | Linter | Default rules (recommended tier) | Strictness lever |\n|----------|--------|----------------------------------|------------------|\n| JS/TS | ESLint | `@eslint/js` + React recommended, import-architecture guards, security plugins, Prettier-compatible | rules block in `eslint.config.js` |\n| JS/TS | Biome | recommended set + full `security` group + console/complexity rules | rule levels in `biome.json` |\n| Python | Ruff | pyflakes / pycodestyle / isort / bugbear / pyupgrade + `S` security + comprehensions / simplify | `select` / `ignore` in `pyproject.toml` |\n| Go | golangci-lint | `standard` set + correctness & quality linters + `gosec` | `linters.enable` in `.golangci.yml` |\n| Rust | Clippy | `pedantic` + `cargo` at `warn`, noisy lints allowed back | `[lints.clippy]` levels in `Cargo.toml` |\n| Terraform | tflint | bundled `terraform` ruleset, `recommended` preset | `preset` in `.tflint.hcl` (`recommended` → `all`) |\n| Ansible | ansible-lint | `moderate` profile | `profile` in `.ansible-lint` (`min` → `production`) |\n| Secrets (all) | gitleaks | default ruleset + key / password rules — **always blocks** | `[allowlist]` in `.gitleaks.toml` |\n\nEvery shipped config carries a **security layer**. gitleaks scans every file in\nevery codebase for secrets (passwords, SSL/private keys, tokens) and always\nblocks — secrets are never baselined. Each linter adds language-specific\nsecurity rules on top (`gosec`, Ruff `S` / flake8-bandit, `eslint-plugin-security`,\nBiome's `security` group), which follow normal progressive baselining.\n\nTo go stricter, pull the lever in the table above — because violations are\nprogressively baselined, raising strictness never blocks existing code, only new\ncode is held to the higher bar. Full per-codebase detail — every default rule\nand how to adjust it — is in\n[`.documentation/standards/lint-rules-guide.md`](.documentation/standards/lint-rules-guide.md).\n\n---\n\n## Quick Start\n\n### Install\n\n```bash\n# Local (recommended — every teammate gets it on clone)\nnpm install --save-dev @theglitchking/gimme-the-lint\nnpx gimme-the-lint install\n\n# Global\nnpm install -g @theglitchking/gimme-the-lint\ngimme-the-lint install\n\n# Claude Code plugin\n/plugin install TheGlitchKing/gimme-the-lint\n```\n\n### First-time setup on an existing project\n\n```bash\nnpx gimme-the-lint install      # writes configs + git hooks\nnpx gimme-the-lint baseline     # captures existing violations as baselines\nnpx gimme-the-lint dashboard    # see what is baselined and any drift\n```\n\nFrom here, every commit is linted — but only your **new** code is held to the\nrules. Commit the `.gtl/` directory so the whole team shares the baseline.\n\n### Day to day\n\nThe pre-commit hook fires on `git commit`. If you introduced a new violation it\nblocks the commit and shows exactly what to fix:\n\n```bash\ngimme-the-lint check --fix      # auto-fix what the linter can\ngit add -A && git commit -m \"…\" # retry\n```\n\n---\n\n## CLI\n\n| Command | Description |\n|---------|-------------|\n| `gimme-the-lint install` | Write configs and set up the project |\n| `gimme-the-lint install --offline` | Air-gapped install — no npm/pip fetches |\n| `gimme-the-lint install --no-baseline` | Greenfield — empty baselines, strict from day one |\n| `gimme-the-lint baseline` | Capture/refresh baselines for every app |\n| `gimme-the-lint baseline --empty` | Write empty baselines (greenfield) |\n| `gimme-the-lint check` | Lint files staged for commit |\n| `gimme-the-lint check --all` | Lint every app, not just staged changes |\n| `gimme-the-lint check --fix` | Auto-fix where the linter supports it |\n| `gimme-the-lint check --strict` | Fail if a linter is missing for present code |\n| `gimme-the-lint check --json` | Full finding list as JSON on stdout, untruncated (triage, agents, CI summaries) |\n| `gimme-the-lint check --fail-on-skip` | Fail if any check could not run — a skip means UNVERIFIED |\n| `gimme-the-lint check --stage=push` | Also run the slower whole-app checks (the contract engine) |\n| `gimme-the-lint materialize` | Write down the API contract your code computes at runtime |\n| `gimme-the-lint verify` | Run the checks that need a database (CI only — never a git hook) |\n| `gimme-the-lint dashboard` | Per-app baseline status + drift |\n| `gimme-the-lint migrate` | Migrate a v1 (`.lttf`) project to the v2 `.gtl/` layout |\n| `gimme-the-lint hooks` | Install pre-commit and pre-push git hooks (honors `core.hooksPath`) |\n| `gimme-the-lint hooks --print pre-push` | Print a snippet to embed in a hook you already own |\n| `gimme-the-lint status` | Overall plugin status |\n| `gimme-the-lint uninstall` | Remove hooks and config |\n\nWiring the Python contract checker into CI **directly** (because the check imports your\napp, and your Python runner may not have Node)? `gtl-contract check` reports findings on\nstdout and **exits 0 even when it finds violations** — pass `--exit-code` to get a status\nyou can gate on (`3` = found violations; `1` stays \"could not check\"). See\n[`.documentation/api/contract-guide.md`](.documentation/api/contract-guide.md#running-gtl-contract-directly-and-its-exit-codes).\n\n> **Upgrading from v2.5?** Re-run `gimme-the-lint hooks`, or the new checks silently\n> never fire. See [`.documentation/procedures/upgrade-guide.md`](.documentation/procedures/upgrade-guide.md)\n> — it carries the full error catalog.\n\n> **Does your repo set `core.hooksPath`?** Re-run `gimme-the-lint hooks`. Every release\n> before 2.8.2 wrote into `.git/hooks` regardless — a directory git never opens once\n> `core.hooksPath` is set — and then reported them installed. `status` now prints the\n> directory it actually read, and flags the leftovers. Repos that already own their hook\n> files should compose with `hooks --print` rather than surrender the file; see\n> [`.documentation/procedures/git-hooks-guide.md`](.documentation/procedures/git-hooks-guide.md).\n\n---\n\n## Configuration\n\nZero config is the default — apps and their linters are auto-detected. To\noverride, add a config file. The canonical location is **`.gtl/config.js`**\n(it travels with the committed `.gtl/` baselines); a repo-root\n`gimme-the-lint.config.js` is also read, for back-compatibility. `install` and\n`migrate` write new configs to `.gtl/`:\n\n```js\nmodule.exports = {\n  // Explicit per-app linter binding (omit `apps` entirely to auto-detect).\n  apps: {\n    'apps/orders-api':    { linters: ['eslint'] },\n    'apps/orders-worker': { linters: ['ruff'] },\n    'apps/billing-events':{ linters: ['golangci-lint'] },\n    'apps/audit-stream':  { linters: ['clippy'] },\n  },\n  // Directories to skip (template/scaffold dirs are skipped by convention).\n  skipPatterns: ['_template-*', '__template__'],\n};\n```\n\n### Polyglot monorepos\n\nA modern monorepo is not one frontend and one backend:\n\n```\napps/\n├── orders-api/        package.json   → eslint\n├── orders-worker/     pyproject.toml → ruff\n├── billing-events/    go.mod         → golangci-lint\n└── audit-stream/      Cargo.toml     → clippy\n```\n\n`gimme-the-lint baseline` discovers all four, binds each to its linter, and\nwrites `.gtl/apps/<app>/baseline.json` per app. Workspace files\n(`pnpm-workspace.yaml`, `nx.json`, `lerna.json`) need no special handling —\neach package carries its own manifest, so discovery just works.\n\n### I use Biome — can I use this?\n\nYes. Drop a `biome.json` in an app and gimme-the-lint binds that app to Biome\ninstead of ESLint — no running both, no doubled CI time, no config conflict.\nBiome's JSON reporter is parsed like any other adapter. (Biome locates\ndiagnostics by byte span, not line number; that is fine — fingerprints exclude\nposition by design.) The linter is config, not a hardcode: ESLint, Biome,\nRuff, golangci-lint and Clippy are all just adapters.\n\n### Idempotent skips\n\nA language is never a hard prerequisite:\n\n- **No code** for a language → silent no-op.\n- **Code present, linter not installed** → loud `⚠ SKIPPED` warning; the commit\n  still goes through, and the gap is recorded in the manifest.\n- Under `--strict` (and in `--offline` installs) that same case **fails loudly**\n  — a silent skip there would hide a provisioning bug.\n\n---\n\n## Adoption modes\n\n**Air-gapped / regulated environments** — `install --offline` performs no\nnetwork fetches, assumes the linter toolchain is provisioned by your image, and\nfails loudly if a present language has no linter:\n\n```bash\ngimme-the-lint install --offline\n```\n\n**Greenfield / new repos** — there is no legacy debt to grandfather, so\n`init --no-baseline` writes empty baselines and installs hooks: every violation\ncounts as new, \"strict from day one\":\n\n```bash\ngimme-the-lint init --no-baseline\n```\n\n---\n\n## Migrating from v1\n\nv2 changes the baseline layout (`.lttf/` + `.lttf-ruff/` → `.gtl/`), the\nbaseline format, and the config schema. One command handles it:\n\n```bash\ngimme-the-lint migrate\n```\n\nIt backs the legacy directories up under `.gtl/legacy-backup/<timestamp>/`,\nthen re-baselines from the current code into the v2 layout. `check` also\ndetects an un-migrated v1 project and prints the same hint. See\n[CHANGELOG.md](CHANGELOG.md) for the full list of breaking changes.\n\n---\n\n## Claude Code\n\n| Command | Description |\n|---------|-------------|\n| `/lint` | Run progressive linting on the project |\n| `/lint:status` | Show the dashboard (per-app baselines + drift) |\n| `/lint:baseline` | Create or refresh baselines |\n\nWhen a commit Claude makes is blocked by the pre-commit hook, the hook output\nincludes LLM instructions: Claude auto-runs `check --fix`, re-stages, and\nretries — only asking you if violations remain after auto-fix.\n\n## GitHub Action\n\n```yaml\n- uses: TheGlitchKing/gimme-the-lint@v2.6.0\n  with:\n    mode: full          # 'full' or 'progressive'\n    fix: false\n    strict: false\n    verify: false       # also run the checks that need a database (CI only)\n    comment-on-pr: true\n```\n\nA ready-to-copy workflow lives at\n[`templates/lint.workflow.template.yml`](templates/lint.workflow.template.yml).\n\n> **Pin a version.** A floating `@v2` tag is only safe if it exists — and until\n> v2.6.0 it did not, so every workflow copied from the old template failed with\n> `unable to find version v2`. `@v2` now exists and moves with each 2.x release; if\n> you would rather not track a moving tag, pin the exact one as above.\n\n---\n\n## Architecture\n\n```\nlib/\n├── violation.js        NormalizedViolation — the linter-agnostic currency\n├── fingerprint.js      line/column-independent violation identity\n├── diff-engine.js      pure diff: new vs baselined vs fixed\n├── baseline-store.js   one baseline.json format for every linter\n├── adapters/           one adapter per linter (eslint, biome, ruff,\n│                       golangci-lint, clippy, tflint, ansible-lint)\n│                       + the base contract\n├── project-model.js    discovers apps + binds them to linters\n├── units.js            resolves apps → {dir, linters, baseline path}\n├── check.js            runCheck: lint → diff → report\n├── baseline.js         runBaseline: capture violations into .gtl/\n├── gtl-manifest.js     global .gtl/manifest.json\n├── drift.js            per-app drift detection\n├── toolchain.js        per-language linter availability\n├── migrate.js          v1 → v2 migration\n└── dashboard.js, report.js, installer.js, …\n```\n\nThe engine is pure and fully unit-tested; adapters wrap real linters; the CLI,\ngit hooks, GitHub Action and Claude Code plugin are thin front doors over it.\n\n## Requirements\n\n- **Node.js** >= 20\n- **Git** (for hooks and staged-file detection)\n- A linter for each language you use (`eslint`/`biome`, `ruff`, `golangci-lint`,\n  `clippy`, `tflint`, `ansible-lint`) — any language whose linter is absent is\n  simply skipped\n\n## License\n\nMIT — see [LICENSE](LICENSE)\n",
  "bytes": 21680,
  "sha": "cabd0ce9dbf00cc0936060da519c383858c35b66d542fa74346ac590e27031b8",
  "repo_slug": "theglitchking/gimme-the-lint",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/plg_theglitchking_gimme_the_lint_gimme_the_l_cc6fdbe9/readme"
}