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 #[must_use], #[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) andassets/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 unfulfilled_lint_expectations = "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
unsafe_code, generated code, a configuration-dependent false positive),
use an honest reasoned #![allow(lint, reason = "…")] instead. Outer
#[allow] is a rejected pattern — allow_attributes deliberately doesn't
see inner attributes, and allow_attributes_without_reason still forces
the reason on both.
Mode A: writing or reviewing Rust
- Read
references/rules.mdin 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 ordynexists, 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 statusand preserve unrelated changes; find the workspace rootCargo.toml, any existingclippy.toml/.clippy.toml, existing[lints]/[workspace.lints]tables, and#![allow(...)]/#![warn(...)]crate attributes that the table will supersede. - Merge
assets/workspace-lints.tomlinto the workspace rootCargo.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::lint_groups_priorityflags 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 withworkspace = 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.tomlinto the repo'sclippy.toml, preserving existingdisallowed-*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 correspondingdisallowed-methodstemplates inclippy.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-targetsand confirm nounknown 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
warnas a migration step rather than sprinkling suppressions. - For CI, recommend:
cargo fmt --all --check, thencargo clippy --workspace --all-targetsover 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 todenyin the table, where the choice is explicit and versioned. Addcargo-denyandcargo-macheteif 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 (redundant_clone, needless_collect,
significant_drop_tightening) that are known to have false positives and
negatives — they are tripwires backing the prose rules, not the policy itself.
indexing_slicing and wildcard_enum_match_arm 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+, unused_result_ok 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 missing_errors_doc = "warn",
missing_panics_doc = "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, indexing_slicing, string_slice, 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.