agentleFS
uwuclxdyuwuclxdy/agenticat/skills/clean-rust/SKILL.md

clean-rust

Skill7 starsChanged 18 days agoMITView on GitHub

Idiomatic Rust 2024 rules: ownership, error handling, async, unsafe, traits, iterators, clippy.

1 other project uses a version of this.

Adopted by

Most recently taken up by

The file

We split this skill into the rules it gives an agent

7 rules. About 0 are common, 1 is distinctive, and 6 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈1

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 6

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    No sentinel returns (-1, "") for failure.

    line 34
  2. Very specific:

    Every .clone() is an explicit cost, never one to silence the borrow checker.

    line 42
  3. Very specific:

    No redundant #[cfg] inside a module that's already cfg-gated at its mod declaration.

    line 86
  4. Very specific:

    Never truncate a String at a raw byte index.

    line 97
  5. Very specific:

    Every lint suppression is justified: #[expect(lint, reason = "...")] over bare #[allow].

    line 109
  6. Distinctive:

    No formatting-only churn in feature PRs: the diff should match the description.

    line 116
  7. Very specific:

    Default posture: unsafe_code = "forbid" until a concrete need exists.

    line 120

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Clean Rust

Rust-specific conventions for writing, reviewing, and refactoring. The core rules below always apply. Load the one reference file matching the task's domain; don't load them all.

If the clean-code skill is installed, its language-agnostic principles (function size, naming hygiene, comment discipline) still apply. Where generic advice conflicts with Rust idiom, this skill wins. Classic examples: "prefer exceptions over error codes" maps to Result, never panics; "replace switch with polymorphism" maps to exhaustive match, which in Rust is a feature, not a smell.

Task touchesFile
Error types, propagation helpers, failure semantics, retriesreferences/error-handling.md
async/await, tokio, select!, channels, spawned child processesreferences/async.md
Threads, atomics, lock ordering, drop orderreferences/concurrency.md
Process-global mutable state (env vars, global overrides) across threadsreferences/edition-2024.md
Public API shape, builders, newtypes, typestate, serdereferences/api-design.md
Any unsafe block, FFI, raw pointers, exported symbolsreferences/unsafe.md
Writing tests or debugging test infrastructurereferences/testing.md
Hot paths, allocations, string building, hasher choicereferences/performance.md
Benchmarks, criterion, #[bench]references/performance.md
Secret files on disk, keys and tokens at restreferences/security.md
Logging, tracing spans, log levelsreferences/observability.md
Edition 2024 migration or 2024-specific behavior changesreferences/edition-2024.md

Errors

  • Result + ? everywhere; no .unwrap()/.expect() outside tests and one-time init of hand-audited literals (LazyLock<Regex> on a fixed pattern). The rare justified expect carries a message naming the invariant that makes it safe.
  • No sentinel returns (-1, "") for failure. Option<T> for absence that isn't an error.
  • thiserror for library errors (callers can match), anyhow/eyre at the application boundary. Never Box<dyn Error> or String as a library error type.
  • Bind a Result once with match/if let/let-else, never .is_ok() followed by separate access or a downstream .unwrap().
  • Pick one failure semantics per operation: best-effort (warn per item, Ok(()) at end) or fail-fast (propagate first error). Never warn-per-item and then fail at the end; that surprises exit-code consumers.

Ownership & Borrowing

  • Borrow by default: &str, &[T], impl AsRef<Path> for read-only params. Never &String or &Vec<T>.
  • Every .clone() is an explicit cost, never one to silence the borrow checker. Cow<'_, str> when allocation is conditional; Arc<T> for shared ownership across threads.
  • Unsigned types for counts, ports, sizes: jobs: u32, not jobs: i32. The type documents the constraint.
  • Let lifetime elision work. A struct with 3+ lifetime params is a design smell: consider owned data.

Naming

RFC 430 casing (snake_case items, CamelCase types, SCREAMING_SNAKE_CASE consts) plus semantic method prefixes:

PrefixContract
into_consumes self, returns owned
as_cheap borrowed view, no allocation
to_possibly expensive, may allocate
try_fallible variant returning Result
is_ / has_boolean query
with_builder-style configuration
_mut suffixmutable variant
  • No get_ for plain field access: fn name(&self) -> &str. Reserve get for fallible lookups (get(key) -> Option<&V>).
  • Concrete names: cmd not executor, typ not ty. Single letters only in tiny scopes.
  • Names must not lie about the data: no _list suffix on a HashMap, no owned_ on a &str.
  • Wrap semantically distinct primitives in newtypes (UserId(u64), OrderId(u64)) so the compiler catches argument swaps.

Control Flow & Iterators

  • match on enums stays exhaustive: no _ => {} arm in dispatchers. Spell out every variant: an empty arm with a comment beats a wildcard that silently swallows the next variant added.
  • let-else for guard-style early returns; it reads linearly where .ok().and_then(...).is_some_and(...) chains don't.
  • Iterator chains for value-producing pipelines. When the result is discarded (let _ = ...), the chain is control flow in disguise; write the for loop.
  • Return impl Iterator<Item = T> when callers consume sequentially; premature .collect() allocates for nothing.
  • Group one logical filter into one .filter_map() closure (using ? and early return None) instead of fragmented .filter_map().filter().filter_map() chains.
  • Don't reimplement the standard library: split_once('=') over .splitn(2, '=').collect(), unwrap_or_default() over unwrap_or_else(|| T::default()). Don't reimplement an in-scope codebase helper either: a fix for a type or signature error lets the existing helper's return type flow through the signature (return the helper's PathBuf where the call site asked for a name) instead of mirroring the helper's body under new return values. Reuse in this order: an in-scope helper, an existing generic config knob, a sibling-code precedent, the standard library, a well-known crate, then hand-rolled; a hand-rolled fallback is fine only when the standard option has a concrete documented limit, and carries an in-code comment naming it.
  • One fluent chain over three named single-use intermediates; declare variables next to first use, not at the top of the function.
  • Prefer chained builder calls over let mut named variables for linear configuration sequences. When each step returns Self, naming the intermediate creates a variable-mixing bug class: two similar blocks in one function, copy-paste produces first.configure() where second.configure() was meant, and the compiler accepts it. Chaining makes each pipeline self-contained.

Types & Traits

  • Derive liberally where semantically correct: Debug on effectively every public type; Clone, PartialEq, Eq, Hash as meaning allows (if a == b then hash(a) == hash(b) must hold); Default when a meaningful empty value exists.
  • dyn Trait only for genuinely open sets (plugins, user extension). A closed set of a few known types is an enum: simpler and faster.
  • Typed #[derive(Deserialize)] structs over serde_json::Value indexing: field typos become compile errors instead of silent runtime Nones.
  • impl Trait in argument position for simple bounds; where clauses when signatures grow. Don't make a function generic when one concrete type is ever used.

Platform & cfg

  • Short forms: #[cfg(windows)], #[cfg(unix)], not #[cfg(target_os = "windows")].
  • No redundant #[cfg] inside a module that's already cfg-gated at its mod declaration.
  • Helpers used on only one platform must be gated (or #[allow(unused)]) so every target compiles warning-free. Cross-platform CI fails on the platform that doesn't use them.
  • An #[allow(dead_code)]/#[allow(unused)] on an item whose only callers sit in cfg-gated modules is load-bearing (the item is live on those platforms, the allow keeps the others warning-free); in a binary crate pub does not exempt an item from dead_code; a sibling's same-file allow is the precedent signal; on such an item keep the bare #[allow]: #[expect(dead_code)] fires unfulfilled_lint_expectations on the platforms where the item is live.

Strings & IO

  • static RE: LazyLock<Regex> for compiled-once regexes; never compile inside a loop or per call.
  • ASCII character classes ([a-zA-Z0-9]) over POSIX [[:alnum:]]: the explicit range reads unambiguously and ports to engines where POSIX classes are locale-sensitive (in Rust's regex they're ASCII-only either way).
  • Inline format args: debug!("found {name:?}"), not debug!("found {:?}", name).
  • Forward child-process output with io::stdout().write_all(&output.stdout)?; println! mangles encoding and panics on broken pipes.
  • Long-form flags when spawning external commands (--force, not -f): the call site is its own documentation.
  • Never truncate a String at a raw byte index. &s[..n] panics mid-codepoint on non-ASCII; the "safe" s.get(..n).unwrap_or(s) is worse: it returns the whole string when n lands mid-codepoint, so oversize input escapes the cap. Use floor_char_boundary(n) or walk char_indices().

Modules & Visibility

  • Private by default; pub(crate) for internal sharing. Fields with invariants stay private behind methods.
  • Organize by domain (order/, user/), not by kind (models/, services/).
  • pub items before private helpers in a file; use statements at the top, never inside functions.

Comments & Docs

  • Comments explain why: hidden constraints, upstream bug links, invariants the reader can't see. Never what the next line does. Delete commented-out code; git remembers.
  • Public APIs get /// docs with # Examples (runnable), plus # Errors, # Panics, # Safety when applicable.
  • Every lint suppression is justified: #[expect(lint, reason = "...")] over bare #[allow].
  • Non-obvious string input formats get documented above the signature (// accepts "name", "name:tag", or "ns/name:tag").

Tooling

  • cargo fmt and cargo clippy clean in CI, no exceptions. Fix warnings; don't blanket-suppress.
  • Libraries: #![warn(clippy::pedantic)] and selectively allow, never a blanket #![allow(clippy::all)]. Consider unwrap_used/expect_used at warn (see references/testing.md for keeping test code exempt).
  • No formatting-only churn in feature PRs: the diff should match the description.

Unsafe

Default posture: unsafe_code = "forbid" until a concrete need exists. When it does exist, every block carries a // SAFETY: comment and stays minimal; full discipline in references/unsafe.md.

Process

  • One PR = one focused change; formatting fixes, new workflows, and features travel separately.
  • A technically-correct change that breaks a user contract (CLI flags, config keys, env semantics) keeps the legacy path, ships the new one alongside, and milestones the removal for the next major. Warn only on genuine old-vs-new conflicts: end users on only-old or only-new see nothing. A change to an existing default ships opt-in/off by default, unless the old behavior is broken for every user (then on by default with an opt-out) or the behavior it breaks is incidental/unsupported (then it ships as the new default without an opt-in).
  • Feature-detect external tools by parsing --version output; where that's unreliable, sniff for specific stderr messages. Either way, probe with plain .output() so a failed probe stays non-fatal.
  • Unexpected or unparseable external-tool output fails loud, with a message naming the command, never a silent fallback; a clean skip is only for a known not-applicable state (missing binary, wrong OS), distinct from "ran, found nothing" and from failure.

Pre-Submit Checklist

  • No .unwrap() outside tests/compile-time constants; no .is_ok() + separate access
  • Test expectations hand-computed, never recomputed by the code under test; no re-pinned paths
  • Failure semantics consistent: best-effort or fail-fast, not mixed; cleanup always runs
  • Borrows by default; no &String/&Vec<T>; every .clone() intentional
  • Method prefixes honest (into_/as_/to_/try_); newtypes for swappable primitives
  • No _ => {} in enum dispatchers; let-else over combinator gymnastics
  • impl Iterator over premature Vec; no reimplementations of stdlib or in-scope helpers
  • Typed Deserialize over Value; dyn only for open sets
  • #[cfg(windows)] short form; single-platform helpers gated for all-target CI
  • LazyLock regexes; inline format args; write_all for child output
  • Comments say why; suppressions carry reason; no commented-out code
  • fmt + clippy clean; no formatting churn outside the change
  • One focused change per PR; breaking changes keep the legacy path, removal milestoned

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.