Strict linting and security scanning for TypeScript, C#, Python, Rust and Java, consumed from one repo instead of rebuilt in every project.
Who this is for: you maintain more than one repo, and you want
typescript-eslint strict-type-checked, Roslyn analyzers, Ruff + mypy strict,
clippy pedantic, plus secret and dependency scanning — without designing that
stack yourself and re-tuning it in every project. You add one file. The rules
live here, and they are versioned here.
Free tools only: OSS analyzers plus the GitHub Actions free tier.
This repo is public and genuinely adoptable, and supported for nobody. Both halves are meant literally, and they are worth two minutes before you wire anything up.
The supported stack. Inside it, things are expected to work and a failure is a bug worth reporting. Outside it, you are out of scope rather than broken:
| Supported | |
|---|---|
| Languages | TypeScript · C# / .NET · Python · Rust · Java (Maven only — Gradle fails loud rather than skipping) |
| Package managers | npm · pnpm · pip / uv · Cargo · Maven. yarn and bun are a hard exit |
| CI | GitHub Actions. Nothing else is attempted |
What you are owed: the tag, and nothing else. Issues and pull requests from outside carry no promise of a response, and no language or tool gets added because someone outside asks for it. What the tag promises:
- A new finding is not a breaking change. New rules, analyzer bumps and
tightened configs land on
@v1and can turn a green build red. That is the product working — ratcheting up is the point — and--changed-onlyis how you grandfather an existing backlog on day one. Every one of them is listed under Rule changes inCHANGELOG.md; read that section before you bump, because it is the whole mitigation for this permission. - A mechanism change is breaking, and gets a new major tag instead: an input removed or renamed, a job renamed, detection behaviour altered, or anything new you must have in your own repo.
- The minor position means something now. A minor bump is a new capability reaching you — a new input, language, gate or config. A patch is fixes and Finding changes that add no new capability. Neither is breaking, and both can turn a build red, so read Rule changes rather than the number.
- A SHA pin is closed over one payload. Each new closed release wrapper
points every first-party action reference at its recorded payload commit, so
a pinned
quality.ymlcannot silently resolve a later@v1action. Use av1.x.ytag when you want a named immutable release, or the published wrapper SHA for a Scorecard-clean pin.@v1remains the moving latest-v1 choice.
What this does not do. Stated because a gate that stays quiet about its edges gets trusted past them:
- No SARIF upload, no code-scanning integration. Findings land in the job log and on the PR diff.
- No
.pre-commit-hooks.yaml. The hook is installed byadopt.sh --hooks, not consumed by the pre-commit framework. - No judgement about whether code should exist. See Limits.
This is the entire adoption. One file, six lines:
# .github/workflows/quality.yml
name: quality
on: [push, pull_request]
jobs:
quality:
uses: maximalcode/maxi-quality/.github/workflows/quality.yml@v1No token, no secrets, no checkout of this repo. Every push and pull request now runs Semgrep, Gitleaks and OSV-Scanner.
Start with the Layer 1 preflight:
python3 maxi-quality/scripts/preflight.py <your-repo> reports bug-class,
stylistic and unclassified findings in a disposable copy. It always exits zero;
an incomplete analysis says so instead of reporting a misleading clean bill.
Same file, two more lines. Your whole existing backlog is grandfathered on day one, and the gate still fails on anything new:
name: quality
on: [push, pull_request]
jobs:
quality:
uses: maximalcode/maxi-quality/.github/workflows/quality.yml@v1
with:
changed-only: origin/main # only fail on code changed since this ref
languages: 'none' # Layer 2 only, for nowWorked example: examples/legacy-ratchet/.
adopt.sh detects your languages, copies the two or three files per language
that ESLint, .NET and Cargo cannot load from a remote repo, and writes the
workflow above:
git clone https://github.com/maximalcode/maxi-quality.git
./maxi-quality/scripts/adopt.sh <your-repo> --dry-run # prints every action, writes nothing
./maxi-quality/scripts/adopt.sh <your-repo>It never overwrites a file without --force, and re-running it is safe.
A new repo goes from git init to failing CI on a planted bug in under ten
minutes, using only this page. CI here asserts it: a repo adopted by the script
rejects the bad fixture with exactly the same 23 errors as the hand-configured
sample. Full walkthrough in docs/ADOPTION.md, copyable
repos in examples/.
flowchart LR
A["your repo<br/>quality.yml, 6 lines"] --> B["maxi-quality<br/>workflow @v1"]
B -->|"detects your<br/>languages"| L1
B --> L2
subgraph L1["Layer 1 — only the languages you have"]
T1["TypeScript<br/>ESLint + tsc"]
T2["C#<br/>Roslyn + Sonar"]
T3["Python<br/>Ruff + mypy"]
T4["Rust<br/>clippy"]
T5["Java<br/>Error Prone"]
end
subgraph L2["Layer 2 — every repo, zero config"]
S1["Semgrep<br/>12 conventions"]
S2["Gitleaks<br/>secrets"]
S3["OSV-Scanner<br/>vulnerable deps"]
end
L1 --> R["pass / fail<br/>+ PR annotations"]
L2 --> R
Layer 2 is identical for every repo, whatever it is written in: Semgrep running this repo's 12 conventions, Gitleaks for secrets, OSV-Scanner for vulnerable dependencies. It needs no config in your repo at all.
Layer 1 is your own compiler and linter turned up: typescript-eslint
strict-type-checked, Roslyn with SonarAnalyzer and Roslynator under
TreatWarningsAsErrors, Ruff with mypy strict, and clippy pedantic with
-Dwarnings. It runs inside your build, so it needs a few files copied in.
| Layer 2 | Layer 1 | |
|---|---|---|
| Scope | every repo, any stack | per language, only the ones you have |
| Config in your repo | none | 2–3 files |
| Runs as | one CI job, no token | your own build and lint step |
| Catches | secrets, vulnerable deps, injection, conventions | type holes, floating promises, dead code, undisposed resources |
| Grandfathers your backlog | yes | no |
Semgrep supports --baseline-commit, so Layer 2 can be told to fail only on
code changed since a given ref. ESLint, Roslyn and mypy have no equivalent: a
rule is either on and failing your build, or off. That makes Layer 1
all-or-nothing per rule, and adopting it on an existing codebase is a cleanup
sprint rather than a config change.
Here is what each layer actually cost on real codebases:
| First-run findings | What it took to go green | |
|---|---|---|
| Layer 2, Consumer A | 57 (70 before rule tuning) | one line of YAML, all deferred |
| Layer 2, Consumer B | 15 (17 before tuning) | same |
| Layer 1 Python, Consumer C | already clean | one line of per-file-ignores |
| Layer 1 C#, Consumer A | 197 (~120 after tuning) | fix them |
| Layer 1 TypeScript, Consumer A | 445 | fix them |
| Layer 1 TypeScript, Consumer B | 4,902 | fix them |
Consumer A is a private C# and TypeScript monorepo, Consumer B a private
TypeScript app, Consumer C a private Python service. The numbers are real, the
names are private. Full detail in docs/STATUS.md.
So turn on Layer 2 first. It is one line, it defers your backlog, and it is the half that catches leaked credentials and vulnerable dependencies. Add Layer 1 one language at a time, when someone has the week to spend on it.
| Area | What it is | Where |
|---|---|---|
| TypeScript | ESLint strict-type-checked + sonarjs, shared tsconfig |
configs/typescript/ |
| C# / .NET | Roslyn + SonarAnalyzer + Roslynator, warnings as errors | configs/dotnet/ |
| Python | Ruff, 13 rule families, plus mypy strict |
configs/python/ |
| Rust | clippy pedantic, rustfmt, cargo-deny, unsafe_code forbidden |
configs/rust/ |
| Java | Error Prone + NullAway at ERROR, -Xlint:all -Werror, Spotless/palantir (AOSP). Maven only |
configs/java/ |
| Custom rules | Semgrep: 12 conventions, 40 rule ids | semgrep/ |
| Dead code and unused dependencies | knip (TypeScript) and deptry (Python), gating the measured issue types only | actions/deadcode/ |
| Secrets and dependencies | Gitleaks and OSV-Scanner behind one runner | scripts/scan.sh |
| CI | reusable workflow, pinned by the @v1 tag |
quality.yml |
| PR feedback | findings annotated on the diff, on by default | #40 |
| Per-repo policy | disable or downgrade rules, exclude paths, add your own | .maxi-quality.yml |
| Pre-commit hook | opt-in, runs on staged content, --no-verify always works |
adopt.sh --hooks |
| Coverage | a ratchet on the whole repo, plus a threshold on the lines a change adds. One line once your test job uploads a report | ADOPTION §8 |
| SBOM and licence gate | both from OSV-Scanner; the SBOM never gates | docs/REFERENCE.md |
| Formatting | Prettier, ruff format, dotnet format whitespace, gated in CI |
#42 |
| Editor defaults | one shared .editorconfig |
configs/editorconfig |
| Editor parity | adopt.sh --editor writes the settings that make the official extensions show what CI shows, for the languages you actually have |
configs/editor/ |
| Agent guard | adopt.sh --agent installs hooks that refuse to end a session on code the gate has not seen, refuse an edit that weakens the test suite, and refuse --no-verify. Every stop decision is appended to a per-checkout ledger — a reason code, never content — so the guard's track record is counted rather than recalled |
configs/agent/ |
| Copyable examples | eight complete consumer repos, each asserted by CI | examples/ |
| Test suite | planted-bug samples per language; a sample that stops failing means the config regressed | samples/ |
- A first-run Layer 1 number says as much about your repo as about the
baseline. Consumer B's 4,902 traces to a single untyped interop boundary
spraying
any. Only 36 of those findings were bug-class, or 0.7%; on Consumer A it was 35 inside 445. Measure before you commit to it. - Layer 1 TypeScript has a version ceiling. typescript-eslint 8.x supports
typescript >=4.8.4 <6.1.0. A repo on TypeScript 7 gets a hard exit, not a degraded run. - 12 Semgrep conventions is not a security product. They are pattern matchers with a known evasion tail, and they sit next to Gitleaks and OSV-Scanner, which do the matching properly.
- There is no dashboard. SonarQube CE was measured and dropped: 1 of 8
planted TypeScript bugs, and no rule id for
no-floating-promises(the evaluation). A weekly report written into a GitHub issue replaced it. Detection is settled; whether a presentation layer is worth adding is being measured separately, and until it reports, the answer is still no. - Your editor does not show what CI shows, and installing the official
extensions does not fix it. Measured against the extensions' own manifests:
the Semgrep extension scans only lines changed since the last commit; the
mypy extension runs a mypy it bundles rather than your pin, and type-checks
only the file in the active tab; rust-analyzer runs
cargo check, so not one clippy lint appears; the C# extension reports analyzer findings for open files only; and VS Code type-checks with the TypeScript it ships rather than the one in yournode_modules. Six divergences —configs/editor/is the frozen contract, and it does not close all of them evenly: most are one settings line, the TypeScript one still needs you to accept the workspace-compiler prompt once, and Java has no in-editor parity at all (Error Prone is a javac plugin; the extension uses the Eclipse compiler).adopt.sh --editorwrites the settings for you, and it refuses rather than merging if you already have a.vscode/settings.json. Semgrep is the one row it leaves out on purpose, and permanently: the rules live in this baseline, not in your tree, so the extension would scan with rules you have not got. Fixing that would not be enough anyway — the extension's only filters are path-based, so it cannot honour therules.disableandrules.warnin your.maxi-quality.yml, and would report findings you switched off.scripts/scan.shis the policy-aware path (ADR 0002). - Java is Maven only, and Gradle fails loud rather than skipping. Gradle gets built when a Gradle consumer exists (#10).
- Java's adoption cost is unmeasured. The config is proven against a representative Spring Boot fixture — zero findings on idiomatic code, 8 on planted bugs — but nobody has run it over an existing Java codebase and counted. The other four languages have that number; this one does not yet (STATUS §5).
- Adding Error Prone to a
-Werrorbuild costs you the finding list on a red run. A javac-Xlintwarning ends the compile before the analyzer's pass, so its findings are missing from that build. The gate stays sound — green still means Error Prone ran — but the first run on an existing codebase can look much smaller than it is (STATUS §4). - The dead-code gate is not an AI-slop detector, and reachability is only covered in two of five languages. knip answers "is this TypeScript file, export or dependency reachable?" and deptry answers "is this Python dependency used?" — both name a falsifiable failure, and neither says anything about who wrote the code, because there is no mechanical signature for that. Unused Python code is a hole this baseline states rather than covers: vulture was measured and declined with numbers — 120 findings over 3.18 KLOC of real code, 0 of them confirmed true positives. C#, Rust and Java get whatever their compiler-integrated analyzer sees within a compilation unit and nothing file-level. The full table is in STATUS §4.
- No gate here can tell you that correct, tested code should not exist, and that is the largest single category of what people mean by "AI slop": code that is plausible, well-formed, locally correct and needed by nobody. Every detector in this repo names a falsifiable defect — unreachable, undeclared, untyped, unfinished. "Nobody needed this" is not falsifiable by a scanner, it is a review judgement, and no amount of rules will make it one. What the baseline buys you against it is indirect and still worth having: every mechanical defect it catches is reviewer attention freed to spend on the question a machine cannot answer. The evidence for the underlying premise is itself thin and this repo says so with numbers — EVAL §2n grades the duplication and churn claims as vendor telemetry and the dead-code claim as unmeasured by anyone.
- It defaults to warning, not failing, when the tool is absent.
v1is a moving tag, so a gate that arrived as a hard requirement would red every already-adopted repo the morning it shipped.dead-code: requireis the line that makes it a gate, and adoption is not finished until it is set. - Nine of ten other free tools were measured and declined.
docs/EVAL-vs-oss-tools.mdscores them against the 103 planted findings insamples/. Onlyeslint-plugin-sonarjscleared the bar. - Every number in
docs/was measured, not estimated. Where something was not measured, it says so.
The credentials in
samples/semgrep/are fake and deliberate. They are bait for thehardcoded-secret-*rules, and CI asserts those rules fire on them, so a secret scanner run against this repo will report findings. That is the intended state, and none of them was ever valid.samples/rust/Cargo.lockpins a crate with a known RustSec advisory for the same reason, target-gated so it never builds. Gitleaks needs no action here, since.gitleaks.tomlallowlists the directory automatically, and GitHub's own secret scanning is told the same thing by.github/secret_scanning.yml. Point other scanners away fromsamples/. Details inSECURITY.mdandsamples/semgrep/README.md.
| You want to | Read |
|---|---|
| put this on a repo | docs/ADOPTION.md |
| copy something that already works | examples/ |
| look up an input, flag or rule id | docs/REFERENCE.md |
| know why it is shaped this way | docs/CONCEPT.md |
| see what is proven, and what it cost | docs/STATUS.md |
| check the tool choices against the field | docs/EVAL-vs-oss-tools.md · docs/EVAL-vs-sonarqube.md · docs/EVAL-vs-diff-cover.md |
| change something here | CONTRIBUTING.md |
Anything still to be built lives in the issue tracker rather than in these documents.