rust-anti-slop
Rust's compiler already rejects most of what a linter must catch in other languages. What remains — and what LLM-generated Rust reliably produces — is code that satisfies the compiler by discarding the evidence it asked for: .unwrap() discards fallibility, cloning past a borrow error discards the ownership analysis, a reflexive Arc<Mutex<T>> discards the sharing design, let = discards #[mustuse], #[allow] discards the diagnostic, todo!() fabricates completion, and an unargued unsafe impl Send fabricates thread-safety. None of these constructs is illegitimate in itself — each is illegitimate without its argument. This skill demands the argument.
Enforcement is layered:
- Mechanical —
assets/workspace-lints.toml (a [workspace.lints]
table: cherry-picked clippy restriction lints plus rustc lints) and assets/clippy.toml (test exemptions, disallowed-* bans). No custom lint engine is needed; unlike TypeScript, the platform ships one.
- Prose —
references/rules.md: the rules no lint can express (clone
justification, expect-message quality, error-type doctrine, abstraction discipline). Read it before writing any substantial Rust.
The escape hatch for a specific, presently known violation is #[expect(lint, reason = "…")] — a suppression that states its case and (with unfulfilledlintexpectations = "deny", included) errors the day it stops being needed. Place it at the narrowest scope that contains the violation: a broad (module- or crate-wide) expectation is fulfilled as long as any occurrence exists, so it never expires and silently admits every future occurrence — an open-ended exception dressed as a finite one. When an entire scope genuinely is the unit of exception (an FFI crate and unsafecode, generated code, a configuration-dependent false positive), use an honest reasoned #![allow(lint, reason = "…")] instead. Outer #[allow] is a rejected pattern — allowattributes deliberately doesn't see inner attributes, and allowattributeswithout_reason still forces the reason on both.
Mode A: writing or reviewing Rust
- Read
references/rules.md in full.
- While writing: follow it, plus whatever the mechanical config enforces if
installed. Match established repository idioms where the alternatives are semantically equivalent — consistency outranks this skill's stylistic and organizational preferences; it never outranks correctness, safety, error causality, lifecycle ownership, or explicit project invariants.
- While reviewing: order findings most severe first — fabricated evidence
(unsafe/Send/transmute misuse) > swallowed errors > panic-as-error-handling > lifecycle gaps (unowned tasks, unbounded queues) > borrow-checker appeasement > structure/abstraction. Every finding must state: the exact symbols involved; which evidence the code discards (validity, ownership, failure causality, lifecycle, safety, exhaustiveness); the concrete failure mode it enables — not "unidiomatic"; the smallest repair (prefer deleting or simplifying ownership over adding abstraction); how to prevent recurrence (a lint, a disallowed-* entry, a test); and what was inferred rather than proven. Raise no finding merely because a function is long, a clone or mutex or dyn exists, or a trait has one implementation — connect the smell to a failure, or drop it.
- Never "fix" a finding by erasing its evidence: no unreasoned suppression;
no error erasure unless erasure is the intentional, documented contract of that boundary; no clone whose sole purpose is ending an inconvenient borrow; no silent weakening of a behavioral assertion.
Mode B: installing the lint policy into a repository
- Inspect before changing: read the repo's agent instructions; check
git status and preserve unrelated changes; find the workspace root Cargo.toml, any existing clippy.toml/.clippy.toml, existing [lints]/[workspace.lints] tables, and #![allow(...)]/#![warn(...)] crate attributes that the table will supersede.
- Merge
assets/workspace-lints.toml into the workspace root Cargo.toml.
Keep every existing lint entry; on conflict, keep the repo's stricter level and report the difference. In a single-crate repo without a workspace, use [lints.rust]/[lints.clippy] directly. Normalize priorities: this table is all named lints, but if the repo's existing table contains group entries (pedantic = "warn", nursery = …), those must sit at a lower priority than the named lints that override them — rewrite them as { level = "…", priority = -1 }, never rely on TOML entry order (same-priority group-vs-lint resolution is undefined; clippy::lintgroupspriority flags it). Preserve deliberate existing priorities and report any conflict.
- Opt members into the workspace table, case by case (skip in single-crate
repos). A member with no [lints] table gets [lints] workspace = true. A member with existing local lint entries CANNOT combine them with workspace = true — Cargo rejects that manifest; either migrate its entries into the workspace table (if they're general) or leave the crate on local lints and merge the anti-slop entries into them, reporting which. A special-profile crate (FFI, no_std, codegen) may stay opted out with a stated reason.
- Merge
assets/clippy.toml into the repo's clippy.toml, preserving
existing disallowed-* entries and config keys.
- Propose architectural bans — often the highest-value step. From the repo's
own structure, identify APIs that should only be reachable through a project-owned seam (unbounded channels, raw tokio::spawn, wall clock, std::env, process::exit, raw fs access, println! in non-CLI crates) and activate the corresponding disallowed-methods templates in clippy.toml — but only where a real replacement already exists and owns actual policy (bounds, supervision, injection, context). Never ban an API whose "replacement" merely renames it; list the candidates without a replacement as recommendations instead.
- Validate against the repo's pinned toolchain: run
cargo clippy --workspace --all-targets and confirm no unknown lint / removed lint / config-parse warnings. Lint names drift across clippy versions; drop or rename entries that this toolchain rejects and report each one.
- If findings appear in existing code, report counts per lint. Fix them only
if the user asked for a cleanup; fix real causes, never launder (step 4 of Mode A). If the volume is large, propose demoting specific deny entries to warn as a migration step rather than sprinkling suppressions.
- For CI, recommend:
cargo fmt --all --check, then
cargo clippy --workspace --all-targets over the repository's existing feature matrix (do not impose --all-features — projects with mutually exclusive features have their own combination strategy). Do NOT append -D warnings: it would promote the deliberately-advisory warn tier (and every default rustc/clippy warning) to failures, erasing the deny/warn distinction this policy encodes — the deny tier already fails the build on its own. A team that wants the warn tier enforced should promote specific lints to deny in the table, where the choice is explicit and versioned. Add cargo-deny and cargo-machete if supply-chain and dependency hygiene are wanted.
- Report: files changed, lints enabled at which levels, entries dropped for
toolchain compatibility, findings remaining.
Tuning expectations
The deny tier is meant to survive contact with real projects; the warn tier includes nursery lints (redundantclone, needlesscollect, significantdroptightening) that are known to have false positives and negatives — they are tripwires backing the prose rules, not the policy itself. indexingslicing and wildcardenummatcharm are the most contested entries: keep them for application code, expect pushback in parser/math-heavy crates, and demote there with a stated reason rather than repo-wide. This profile is deliberately orthogonal to style — it can coexist with or without clippy::pedantic; add pedantic separately if the team wants taste enforced, and never enable clippy::restriction as a group.
Toolchain floor: the full policy requires Rust 1.82+ ([lints] table 1.74+, #[expect]/reason 1.81+, unusedresultok 1.82+). Below that, run in compatibility mode: omit unsupported entries and substitute reasoned #[allow] where #[expect] is unavailable — and say so in the report, since the self-expiring suppression model is the part that changes.
Per-crate escalations, applied as guidance rather than shipped as separate profiles: public libraries add missingerrorsdoc = "warn", missingpanicsdoc = "warn" (failure modes are part of the contract) and keep avoid-breaking-exported-api = true; internal applications may set it to false in clippy.toml for full API-shape coverage. Safety-critical or no-panic crates escalate panic, indexingslicing, stringslice, and even expect_used to deny — a level that would be overkill as the default.
The last test for every rule, mechanical or prose: could following it cause an agent to replace a correct, explicit design with a more abstract or more complicated one solely to satisfy the wording? Where the answer is yes, the rule's job is to demand the design's argument — not to prescribe a syntax or an architecture.