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) 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 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.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::lint_groups_priority 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 (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.
1---2name: rust-anti-slop3description: Opinionated guardrails that reject low-evidence Rust patterns — unwrap/clone/Arc<Mutex> reflexes, silent error swallowing, lint suppression, stub completion. Use when writing or reviewing Rust code, or when asked to install anti-slop lint policy (workspace lints + clippy.toml) into a Rust repository.4---5
6# rust-anti-slop
7
8Rust's compiler already rejects most of what a linter must catch in other
9languages. What remains — and what LLM-generated Rust reliably produces — is
10code that *satisfies* the compiler by discarding the evidence it asked for:
11`.unwrap()` discards fallibility, cloning past a borrow error discards the
12ownership analysis, a reflexive `Arc<Mutex<T>>` discards the sharing design,
13`let _ =` discards `#[must_use]`, `#[allow]` discards the diagnostic,
14`todo!()` fabricates completion, and an unargued `unsafe impl Send`
15fabricates thread-safety. None of these constructs is illegitimate in itself
16— each is illegitimate *without its argument*. This skill demands the
17argument.
18
19Enforcement is layered:
20
211. **Mechanical** — `assets/workspace-lints.toml` (a `[workspace.lints]`
22 table: cherry-picked clippy restriction lints plus rustc lints) and
23 `assets/clippy.toml` (test exemptions, `disallowed-*` bans). No custom lint
24 engine is needed; unlike TypeScript, the platform ships one.
252. **Prose** — `references/rules.md`: the rules no lint can express (clone
26 justification, expect-message quality, error-type doctrine, abstraction
27 discipline). Read it before writing any substantial Rust.
28
29The escape hatch for a specific, presently known violation is
30`#[expect(lint, reason = "…")]` — a suppression that states its case and
31(with `unfulfilled_lint_expectations = "deny"`, included) errors the day it
32stops being needed. Place it at the narrowest scope that contains the
33violation: a broad (module- or crate-wide) expectation is fulfilled as long
34as *any* occurrence exists, so it never expires and silently admits every
35future occurrence — an open-ended exception dressed as a finite one. When an
36entire scope genuinely is the unit of exception (an FFI crate and
37`unsafe_code`, generated code, a configuration-dependent false positive),
38use an honest *reasoned* `#![allow(lint, reason = "…")]` instead. Outer
39`#[allow]` is a rejected pattern — `allow_attributes` deliberately doesn't
40see inner attributes, and `allow_attributes_without_reason` still forces
41the reason on both.
42
43## Mode A: writing or reviewing Rust
44
451. Read `references/rules.md` in full.
462. While writing: follow it, plus whatever the mechanical config enforces if
47 installed. Match established repository idioms where the alternatives are
48 semantically equivalent — consistency outranks this skill's stylistic and
49 organizational preferences; it never outranks correctness, safety, error
50 causality, lifecycle ownership, or explicit project invariants.
513. While reviewing: order findings most severe first — fabricated evidence
52 (unsafe/Send/transmute misuse) > swallowed errors > panic-as-error-handling
53 > lifecycle gaps (unowned tasks, unbounded queues) > borrow-checker
54 appeasement > structure/abstraction. Every finding must state: the exact
55 symbols involved; **which evidence the code discards** (validity,
56 ownership, failure causality, lifecycle, safety, exhaustiveness); the
57 **concrete failure mode** it enables — not "unidiomatic"; the **smallest
58 repair** (prefer deleting or simplifying ownership over adding
59 abstraction); how to **prevent recurrence** (a lint, a `disallowed-*`
60 entry, a test); and what was inferred rather than proven. Raise no finding
61 merely because a function is long, a clone or mutex or `dyn` exists, or a
62 trait has one implementation — connect the smell to a failure, or drop it.
634. Never "fix" a finding by erasing its evidence: no unreasoned suppression;
64 no error erasure unless erasure is the intentional, documented contract of
65 that boundary; no clone whose sole purpose is ending an inconvenient
66 borrow; no silent weakening of a behavioral assertion.
67
68## Mode B: installing the lint policy into a repository
69
701. Inspect before changing: read the repo's agent instructions; check
71 `git status` and preserve unrelated changes; find the workspace root
72 `Cargo.toml`, any existing `clippy.toml`/`.clippy.toml`, existing
73 `[lints]`/`[workspace.lints]` tables, and `#![allow(...)]`/`#![warn(...)]`
74 crate attributes that the table will supersede.
752. Merge `assets/workspace-lints.toml` into the workspace root `Cargo.toml`.
76 Keep every existing lint entry; on conflict, keep the repo's stricter
77 level and report the difference. In a single-crate repo without a
78 workspace, use `[lints.rust]`/`[lints.clippy]` directly. Normalize
79 priorities: this table is all named lints, but if the repo's existing
80 table contains *group* entries (`pedantic = "warn"`, `nursery = …`),
81 those must sit at a lower priority than the named lints that override
82 them — rewrite them as `{ level = "…", priority = -1 }`, never rely on
83 TOML entry order (same-priority group-vs-lint resolution is undefined;
84 `clippy::lint_groups_priority` flags it). Preserve deliberate existing
85 priorities and report any conflict.
863. Opt members into the workspace table, case by case (skip in single-crate
87 repos). A member with no `[lints]` table gets `[lints] workspace = true`.
88 A member with existing local lint entries CANNOT combine them with
89 `workspace = true` — Cargo rejects that manifest; either migrate its
90 entries into the workspace table (if they're general) or leave the crate
91 on local lints and merge the anti-slop entries into them, reporting which.
92 A special-profile crate (FFI, no_std, codegen) may stay opted out with a
93 stated reason.
944. Merge `assets/clippy.toml` into the repo's `clippy.toml`, preserving
95 existing `disallowed-*` entries and config keys.
965. Propose architectural bans — often the highest-value step. From the repo's
97 own structure, identify APIs that should only be reachable through a
98 project-owned seam (unbounded channels, raw `tokio::spawn`, wall clock,
99 `std::env`, `process::exit`, raw fs access, `println!` in non-CLI crates)
100 and activate the corresponding `disallowed-methods` templates in
101 `clippy.toml` — but only where a real replacement already exists and owns
102 actual policy (bounds, supervision, injection, context). Never ban an API
103 whose "replacement" merely renames it; list the candidates without a
104 replacement as recommendations instead.
1056. Validate against the repo's pinned toolchain: run
106 `cargo clippy --workspace --all-targets` and confirm no
107 `unknown lint` / `removed lint` / config-parse warnings. Lint names drift
108 across clippy versions; drop or rename entries that this toolchain
109 rejects and report each one.
1107. If findings appear in existing code, report counts per lint. Fix them only
111 if the user asked for a cleanup; fix real causes, never launder (step 4 of
112 Mode A). If the volume is large, propose demoting specific deny entries to
113 `warn` as a migration step rather than sprinkling suppressions.
1148. For CI, recommend: `cargo fmt --all --check`, then
115 `cargo clippy --workspace --all-targets` over the repository's *existing*
116 feature matrix (do not impose `--all-features` — projects with mutually
117 exclusive features have their own combination strategy). Do NOT append
118 `-D warnings`: it would promote the deliberately-advisory warn tier (and
119 every default rustc/clippy warning) to failures, erasing the deny/warn
120 distinction this policy encodes — the deny tier already fails the build
121 on its own. A team that wants the warn tier enforced should promote
122 specific lints to `deny` in the table, where the choice is explicit and
123 versioned. Add `cargo-deny` and `cargo-machete` if supply-chain and
124 dependency hygiene are wanted.
1259. Report: files changed, lints enabled at which levels, entries dropped for
126 toolchain compatibility, findings remaining.
127
128## Tuning expectations
129
130The deny tier is meant to survive contact with real projects; the warn tier
131includes nursery lints (`redundant_clone`, `needless_collect`,
132`significant_drop_tightening`) that are known to have false positives and
133negatives — they are tripwires backing the prose rules, not the policy itself.
134`indexing_slicing` and `wildcard_enum_match_arm` are the most contested
135entries: keep them for application code, expect pushback in parser/math-heavy
136crates, and demote there with a stated reason rather than repo-wide. This
137profile is deliberately orthogonal to style — it can coexist with or without
138`clippy::pedantic`; add pedantic separately if the team wants taste enforced,
139and never enable `clippy::restriction` as a group.
140
141Toolchain floor: the full policy requires Rust 1.82+ (`[lints]` table 1.74+,
142`#[expect]`/`reason` 1.81+, `unused_result_ok` 1.82+). Below that, run in
143compatibility mode: omit unsupported entries and substitute reasoned
144`#[allow]` where `#[expect]` is unavailable — and say so in the report, since
145the self-expiring suppression model is the part that changes.
146
147Per-crate escalations, applied as guidance rather than shipped as separate
148profiles: public libraries add `missing_errors_doc = "warn"`,
149`missing_panics_doc = "warn"` (failure modes are part of the contract) and
150keep `avoid-breaking-exported-api = true`; internal applications may set it
151to `false` in `clippy.toml` for full API-shape coverage. Safety-critical or
152no-panic crates escalate `panic`, `indexing_slicing`, `string_slice`, and
153even `expect_used` to `deny` — a level that would be overkill as the default.
154
155The last test for every rule, mechanical or prose: could following it cause
156an agent to replace a correct, explicit design with a more abstract or more
157complicated one solely to satisfy the wording? Where the answer is yes, the
158rule's job is to demand the design's argument — not to prescribe a syntax or
159an architecture.